修改后台权限
This commit is contained in:
150
node_modules/commander/lib/argument.js
generated
vendored
Normal file
150
node_modules/commander/lib/argument.js
generated
vendored
Normal file
@@ -0,0 +1,150 @@
|
||||
const { InvalidArgumentError } = require('./error.js');
|
||||
|
||||
class Argument {
|
||||
/**
|
||||
* Initialize a new command argument with the given name and description.
|
||||
* The default is that the argument is required, and you can explicitly
|
||||
* indicate this with <> around the name. Put [] around the name for an optional argument.
|
||||
*
|
||||
* @param {string} name
|
||||
* @param {string} [description]
|
||||
*/
|
||||
|
||||
constructor(name, description) {
|
||||
this.description = description || '';
|
||||
this.variadic = false;
|
||||
this.parseArg = undefined;
|
||||
this.defaultValue = undefined;
|
||||
this.defaultValueDescription = undefined;
|
||||
this.argChoices = undefined;
|
||||
|
||||
switch (name[0]) {
|
||||
case '<': // e.g. <required>
|
||||
this.required = true;
|
||||
this._name = name.slice(1, -1);
|
||||
break;
|
||||
case '[': // e.g. [optional]
|
||||
this.required = false;
|
||||
this._name = name.slice(1, -1);
|
||||
break;
|
||||
default:
|
||||
this.required = true;
|
||||
this._name = name;
|
||||
break;
|
||||
}
|
||||
|
||||
if (this._name.endsWith('...')) {
|
||||
this.variadic = true;
|
||||
this._name = this._name.slice(0, -3);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return argument name.
|
||||
*
|
||||
* @return {string}
|
||||
*/
|
||||
|
||||
name() {
|
||||
return this._name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @package
|
||||
*/
|
||||
|
||||
_collectValue(value, previous) {
|
||||
if (previous === this.defaultValue || !Array.isArray(previous)) {
|
||||
return [value];
|
||||
}
|
||||
|
||||
previous.push(value);
|
||||
return previous;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the default value, and optionally supply the description to be displayed in the help.
|
||||
*
|
||||
* @param {*} value
|
||||
* @param {string} [description]
|
||||
* @return {Argument}
|
||||
*/
|
||||
|
||||
default(value, description) {
|
||||
this.defaultValue = value;
|
||||
this.defaultValueDescription = description;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the custom handler for processing CLI command arguments into argument values.
|
||||
*
|
||||
* @param {Function} [fn]
|
||||
* @return {Argument}
|
||||
*/
|
||||
|
||||
argParser(fn) {
|
||||
this.parseArg = fn;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Only allow argument value to be one of choices.
|
||||
*
|
||||
* @param {string[]} values
|
||||
* @return {Argument}
|
||||
*/
|
||||
|
||||
choices(values) {
|
||||
this.argChoices = values.slice();
|
||||
this.parseArg = (arg, previous) => {
|
||||
if (!this.argChoices.includes(arg)) {
|
||||
throw new InvalidArgumentError(
|
||||
`Allowed choices are ${this.argChoices.join(', ')}.`,
|
||||
);
|
||||
}
|
||||
if (this.variadic) {
|
||||
return this._collectValue(arg, previous);
|
||||
}
|
||||
return arg;
|
||||
};
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make argument required.
|
||||
*
|
||||
* @returns {Argument}
|
||||
*/
|
||||
argRequired() {
|
||||
this.required = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make argument optional.
|
||||
*
|
||||
* @returns {Argument}
|
||||
*/
|
||||
argOptional() {
|
||||
this.required = false;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes an argument and returns its human readable equivalent for help usage.
|
||||
*
|
||||
* @param {Argument} arg
|
||||
* @return {string}
|
||||
* @private
|
||||
*/
|
||||
|
||||
function humanReadableArgName(arg) {
|
||||
const nameOutput = arg.name() + (arg.variadic === true ? '...' : '');
|
||||
|
||||
return arg.required ? '<' + nameOutput + '>' : '[' + nameOutput + ']';
|
||||
}
|
||||
|
||||
exports.Argument = Argument;
|
||||
exports.humanReadableArgName = humanReadableArgName;
|
||||
2777
node_modules/commander/lib/command.js
generated
vendored
Normal file
2777
node_modules/commander/lib/command.js
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
39
node_modules/commander/lib/error.js
generated
vendored
Normal file
39
node_modules/commander/lib/error.js
generated
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* CommanderError class
|
||||
*/
|
||||
class CommanderError extends Error {
|
||||
/**
|
||||
* Constructs the CommanderError class
|
||||
* @param {number} exitCode suggested exit code which could be used with process.exit
|
||||
* @param {string} code an id string representing the error
|
||||
* @param {string} message human-readable description of the error
|
||||
*/
|
||||
constructor(exitCode, code, message) {
|
||||
super(message);
|
||||
// properly capture stack trace in Node.js
|
||||
Error.captureStackTrace(this, this.constructor);
|
||||
this.name = this.constructor.name;
|
||||
this.code = code;
|
||||
this.exitCode = exitCode;
|
||||
this.nestedError = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* InvalidArgumentError class
|
||||
*/
|
||||
class InvalidArgumentError extends CommanderError {
|
||||
/**
|
||||
* Constructs the InvalidArgumentError class
|
||||
* @param {string} [message] explanation of why argument is invalid
|
||||
*/
|
||||
constructor(message) {
|
||||
super(1, 'commander.invalidArgument', message);
|
||||
// properly capture stack trace in Node.js
|
||||
Error.captureStackTrace(this, this.constructor);
|
||||
this.name = this.constructor.name;
|
||||
}
|
||||
}
|
||||
|
||||
exports.CommanderError = CommanderError;
|
||||
exports.InvalidArgumentError = InvalidArgumentError;
|
||||
747
node_modules/commander/lib/help.js
generated
vendored
Normal file
747
node_modules/commander/lib/help.js
generated
vendored
Normal file
@@ -0,0 +1,747 @@
|
||||
const { humanReadableArgName } = require('./argument.js');
|
||||
|
||||
/**
|
||||
* TypeScript import types for JSDoc, used by Visual Studio Code IntelliSense and `npm run typescript-checkJS`
|
||||
* https://www.typescriptlang.org/docs/handbook/jsdoc-supported-types.html#import-types
|
||||
* @typedef { import("./argument.js").Argument } Argument
|
||||
* @typedef { import("./command.js").Command } Command
|
||||
* @typedef { import("./option.js").Option } Option
|
||||
*/
|
||||
|
||||
// Although this is a class, methods are static in style to allow override using subclass or just functions.
|
||||
class Help {
|
||||
constructor() {
|
||||
this.helpWidth = undefined;
|
||||
this.minWidthToWrap = 40;
|
||||
this.sortSubcommands = false;
|
||||
this.sortOptions = false;
|
||||
this.showGlobalOptions = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* prepareContext is called by Commander after applying overrides from `Command.configureHelp()`
|
||||
* and just before calling `formatHelp()`.
|
||||
*
|
||||
* Commander just uses the helpWidth and the rest is provided for optional use by more complex subclasses.
|
||||
*
|
||||
* @param {{ error?: boolean, helpWidth?: number, outputHasColors?: boolean }} contextOptions
|
||||
*/
|
||||
prepareContext(contextOptions) {
|
||||
this.helpWidth = this.helpWidth ?? contextOptions.helpWidth ?? 80;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an array of the visible subcommands. Includes a placeholder for the implicit help command, if there is one.
|
||||
*
|
||||
* @param {Command} cmd
|
||||
* @returns {Command[]}
|
||||
*/
|
||||
|
||||
visibleCommands(cmd) {
|
||||
const visibleCommands = cmd.commands.filter((cmd) => !cmd._hidden);
|
||||
const helpCommand = cmd._getHelpCommand();
|
||||
if (helpCommand && !helpCommand._hidden) {
|
||||
visibleCommands.push(helpCommand);
|
||||
}
|
||||
if (this.sortSubcommands) {
|
||||
visibleCommands.sort((a, b) => {
|
||||
// @ts-ignore: because overloaded return type
|
||||
return a.name().localeCompare(b.name());
|
||||
});
|
||||
}
|
||||
return visibleCommands;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare options for sort.
|
||||
*
|
||||
* @param {Option} a
|
||||
* @param {Option} b
|
||||
* @returns {number}
|
||||
*/
|
||||
compareOptions(a, b) {
|
||||
const getSortKey = (option) => {
|
||||
// WYSIWYG for order displayed in help. Short used for comparison if present. No special handling for negated.
|
||||
return option.short
|
||||
? option.short.replace(/^-/, '')
|
||||
: option.long.replace(/^--/, '');
|
||||
};
|
||||
return getSortKey(a).localeCompare(getSortKey(b));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an array of the visible options. Includes a placeholder for the implicit help option, if there is one.
|
||||
*
|
||||
* @param {Command} cmd
|
||||
* @returns {Option[]}
|
||||
*/
|
||||
|
||||
visibleOptions(cmd) {
|
||||
const visibleOptions = cmd.options.filter((option) => !option.hidden);
|
||||
// Built-in help option.
|
||||
const helpOption = cmd._getHelpOption();
|
||||
if (helpOption && !helpOption.hidden) {
|
||||
// Automatically hide conflicting flags. Bit dubious but a historical behaviour that is convenient for single-command programs.
|
||||
const removeShort = helpOption.short && cmd._findOption(helpOption.short);
|
||||
const removeLong = helpOption.long && cmd._findOption(helpOption.long);
|
||||
if (!removeShort && !removeLong) {
|
||||
visibleOptions.push(helpOption); // no changes needed
|
||||
} else if (helpOption.long && !removeLong) {
|
||||
visibleOptions.push(
|
||||
cmd.createOption(helpOption.long, helpOption.description),
|
||||
);
|
||||
} else if (helpOption.short && !removeShort) {
|
||||
visibleOptions.push(
|
||||
cmd.createOption(helpOption.short, helpOption.description),
|
||||
);
|
||||
}
|
||||
}
|
||||
if (this.sortOptions) {
|
||||
visibleOptions.sort(this.compareOptions);
|
||||
}
|
||||
return visibleOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an array of the visible global options. (Not including help.)
|
||||
*
|
||||
* @param {Command} cmd
|
||||
* @returns {Option[]}
|
||||
*/
|
||||
|
||||
visibleGlobalOptions(cmd) {
|
||||
if (!this.showGlobalOptions) return [];
|
||||
|
||||
const globalOptions = [];
|
||||
for (
|
||||
let ancestorCmd = cmd.parent;
|
||||
ancestorCmd;
|
||||
ancestorCmd = ancestorCmd.parent
|
||||
) {
|
||||
const visibleOptions = ancestorCmd.options.filter(
|
||||
(option) => !option.hidden,
|
||||
);
|
||||
globalOptions.push(...visibleOptions);
|
||||
}
|
||||
if (this.sortOptions) {
|
||||
globalOptions.sort(this.compareOptions);
|
||||
}
|
||||
return globalOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an array of the arguments if any have a description.
|
||||
*
|
||||
* @param {Command} cmd
|
||||
* @returns {Argument[]}
|
||||
*/
|
||||
|
||||
visibleArguments(cmd) {
|
||||
// Side effect! Apply the legacy descriptions before the arguments are displayed.
|
||||
if (cmd._argsDescription) {
|
||||
cmd.registeredArguments.forEach((argument) => {
|
||||
argument.description =
|
||||
argument.description || cmd._argsDescription[argument.name()] || '';
|
||||
});
|
||||
}
|
||||
|
||||
// If there are any arguments with a description then return all the arguments.
|
||||
if (cmd.registeredArguments.find((argument) => argument.description)) {
|
||||
return cmd.registeredArguments;
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the command term to show in the list of subcommands.
|
||||
*
|
||||
* @param {Command} cmd
|
||||
* @returns {string}
|
||||
*/
|
||||
|
||||
subcommandTerm(cmd) {
|
||||
// Legacy. Ignores custom usage string, and nested commands.
|
||||
const args = cmd.registeredArguments
|
||||
.map((arg) => humanReadableArgName(arg))
|
||||
.join(' ');
|
||||
return (
|
||||
cmd._name +
|
||||
(cmd._aliases[0] ? '|' + cmd._aliases[0] : '') +
|
||||
(cmd.options.length ? ' [options]' : '') + // simplistic check for non-help option
|
||||
(args ? ' ' + args : '')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the option term to show in the list of options.
|
||||
*
|
||||
* @param {Option} option
|
||||
* @returns {string}
|
||||
*/
|
||||
|
||||
optionTerm(option) {
|
||||
return option.flags;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the argument term to show in the list of arguments.
|
||||
*
|
||||
* @param {Argument} argument
|
||||
* @returns {string}
|
||||
*/
|
||||
|
||||
argumentTerm(argument) {
|
||||
return argument.name();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the longest command term length.
|
||||
*
|
||||
* @param {Command} cmd
|
||||
* @param {Help} helper
|
||||
* @returns {number}
|
||||
*/
|
||||
|
||||
longestSubcommandTermLength(cmd, helper) {
|
||||
return helper.visibleCommands(cmd).reduce((max, command) => {
|
||||
return Math.max(
|
||||
max,
|
||||
this.displayWidth(
|
||||
helper.styleSubcommandTerm(helper.subcommandTerm(command)),
|
||||
),
|
||||
);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the longest option term length.
|
||||
*
|
||||
* @param {Command} cmd
|
||||
* @param {Help} helper
|
||||
* @returns {number}
|
||||
*/
|
||||
|
||||
longestOptionTermLength(cmd, helper) {
|
||||
return helper.visibleOptions(cmd).reduce((max, option) => {
|
||||
return Math.max(
|
||||
max,
|
||||
this.displayWidth(helper.styleOptionTerm(helper.optionTerm(option))),
|
||||
);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the longest global option term length.
|
||||
*
|
||||
* @param {Command} cmd
|
||||
* @param {Help} helper
|
||||
* @returns {number}
|
||||
*/
|
||||
|
||||
longestGlobalOptionTermLength(cmd, helper) {
|
||||
return helper.visibleGlobalOptions(cmd).reduce((max, option) => {
|
||||
return Math.max(
|
||||
max,
|
||||
this.displayWidth(helper.styleOptionTerm(helper.optionTerm(option))),
|
||||
);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the longest argument term length.
|
||||
*
|
||||
* @param {Command} cmd
|
||||
* @param {Help} helper
|
||||
* @returns {number}
|
||||
*/
|
||||
|
||||
longestArgumentTermLength(cmd, helper) {
|
||||
return helper.visibleArguments(cmd).reduce((max, argument) => {
|
||||
return Math.max(
|
||||
max,
|
||||
this.displayWidth(
|
||||
helper.styleArgumentTerm(helper.argumentTerm(argument)),
|
||||
),
|
||||
);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the command usage to be displayed at the top of the built-in help.
|
||||
*
|
||||
* @param {Command} cmd
|
||||
* @returns {string}
|
||||
*/
|
||||
|
||||
commandUsage(cmd) {
|
||||
// Usage
|
||||
let cmdName = cmd._name;
|
||||
if (cmd._aliases[0]) {
|
||||
cmdName = cmdName + '|' + cmd._aliases[0];
|
||||
}
|
||||
let ancestorCmdNames = '';
|
||||
for (
|
||||
let ancestorCmd = cmd.parent;
|
||||
ancestorCmd;
|
||||
ancestorCmd = ancestorCmd.parent
|
||||
) {
|
||||
ancestorCmdNames = ancestorCmd.name() + ' ' + ancestorCmdNames;
|
||||
}
|
||||
return ancestorCmdNames + cmdName + ' ' + cmd.usage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the description for the command.
|
||||
*
|
||||
* @param {Command} cmd
|
||||
* @returns {string}
|
||||
*/
|
||||
|
||||
commandDescription(cmd) {
|
||||
// @ts-ignore: because overloaded return type
|
||||
return cmd.description();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the subcommand summary to show in the list of subcommands.
|
||||
* (Fallback to description for backwards compatibility.)
|
||||
*
|
||||
* @param {Command} cmd
|
||||
* @returns {string}
|
||||
*/
|
||||
|
||||
subcommandDescription(cmd) {
|
||||
// @ts-ignore: because overloaded return type
|
||||
return cmd.summary() || cmd.description();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the option description to show in the list of options.
|
||||
*
|
||||
* @param {Option} option
|
||||
* @return {string}
|
||||
*/
|
||||
|
||||
optionDescription(option) {
|
||||
const extraInfo = [];
|
||||
|
||||
if (option.argChoices) {
|
||||
extraInfo.push(
|
||||
// use stringify to match the display of the default value
|
||||
`choices: ${option.argChoices.map((choice) => JSON.stringify(choice)).join(', ')}`,
|
||||
);
|
||||
}
|
||||
if (option.defaultValue !== undefined) {
|
||||
// default for boolean and negated more for programmer than end user,
|
||||
// but show true/false for boolean option as may be for hand-rolled env or config processing.
|
||||
const showDefault =
|
||||
option.required ||
|
||||
option.optional ||
|
||||
(option.isBoolean() && typeof option.defaultValue === 'boolean');
|
||||
if (showDefault) {
|
||||
extraInfo.push(
|
||||
`default: ${option.defaultValueDescription || JSON.stringify(option.defaultValue)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
// preset for boolean and negated are more for programmer than end user
|
||||
if (option.presetArg !== undefined && option.optional) {
|
||||
extraInfo.push(`preset: ${JSON.stringify(option.presetArg)}`);
|
||||
}
|
||||
if (option.envVar !== undefined) {
|
||||
extraInfo.push(`env: ${option.envVar}`);
|
||||
}
|
||||
if (extraInfo.length > 0) {
|
||||
const extraDescription = `(${extraInfo.join(', ')})`;
|
||||
if (option.description) {
|
||||
return `${option.description} ${extraDescription}`;
|
||||
}
|
||||
return extraDescription;
|
||||
}
|
||||
|
||||
return option.description;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the argument description to show in the list of arguments.
|
||||
*
|
||||
* @param {Argument} argument
|
||||
* @return {string}
|
||||
*/
|
||||
|
||||
argumentDescription(argument) {
|
||||
const extraInfo = [];
|
||||
if (argument.argChoices) {
|
||||
extraInfo.push(
|
||||
// use stringify to match the display of the default value
|
||||
`choices: ${argument.argChoices.map((choice) => JSON.stringify(choice)).join(', ')}`,
|
||||
);
|
||||
}
|
||||
if (argument.defaultValue !== undefined) {
|
||||
extraInfo.push(
|
||||
`default: ${argument.defaultValueDescription || JSON.stringify(argument.defaultValue)}`,
|
||||
);
|
||||
}
|
||||
if (extraInfo.length > 0) {
|
||||
const extraDescription = `(${extraInfo.join(', ')})`;
|
||||
if (argument.description) {
|
||||
return `${argument.description} ${extraDescription}`;
|
||||
}
|
||||
return extraDescription;
|
||||
}
|
||||
return argument.description;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a list of items, given a heading and an array of formatted items.
|
||||
*
|
||||
* @param {string} heading
|
||||
* @param {string[]} items
|
||||
* @param {Help} helper
|
||||
* @returns string[]
|
||||
*/
|
||||
formatItemList(heading, items, helper) {
|
||||
if (items.length === 0) return [];
|
||||
|
||||
return [helper.styleTitle(heading), ...items, ''];
|
||||
}
|
||||
|
||||
/**
|
||||
* Group items by their help group heading.
|
||||
*
|
||||
* @param {Command[] | Option[]} unsortedItems
|
||||
* @param {Command[] | Option[]} visibleItems
|
||||
* @param {Function} getGroup
|
||||
* @returns {Map<string, Command[] | Option[]>}
|
||||
*/
|
||||
groupItems(unsortedItems, visibleItems, getGroup) {
|
||||
const result = new Map();
|
||||
// Add groups in order of appearance in unsortedItems.
|
||||
unsortedItems.forEach((item) => {
|
||||
const group = getGroup(item);
|
||||
if (!result.has(group)) result.set(group, []);
|
||||
});
|
||||
// Add items in order of appearance in visibleItems.
|
||||
visibleItems.forEach((item) => {
|
||||
const group = getGroup(item);
|
||||
if (!result.has(group)) {
|
||||
result.set(group, []);
|
||||
}
|
||||
result.get(group).push(item);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the built-in help text.
|
||||
*
|
||||
* @param {Command} cmd
|
||||
* @param {Help} helper
|
||||
* @returns {string}
|
||||
*/
|
||||
|
||||
formatHelp(cmd, helper) {
|
||||
const termWidth = helper.padWidth(cmd, helper);
|
||||
const helpWidth = helper.helpWidth ?? 80; // in case prepareContext() was not called
|
||||
|
||||
function callFormatItem(term, description) {
|
||||
return helper.formatItem(term, termWidth, description, helper);
|
||||
}
|
||||
|
||||
// Usage
|
||||
let output = [
|
||||
`${helper.styleTitle('Usage:')} ${helper.styleUsage(helper.commandUsage(cmd))}`,
|
||||
'',
|
||||
];
|
||||
|
||||
// Description
|
||||
const commandDescription = helper.commandDescription(cmd);
|
||||
if (commandDescription.length > 0) {
|
||||
output = output.concat([
|
||||
helper.boxWrap(
|
||||
helper.styleCommandDescription(commandDescription),
|
||||
helpWidth,
|
||||
),
|
||||
'',
|
||||
]);
|
||||
}
|
||||
|
||||
// Arguments
|
||||
const argumentList = helper.visibleArguments(cmd).map((argument) => {
|
||||
return callFormatItem(
|
||||
helper.styleArgumentTerm(helper.argumentTerm(argument)),
|
||||
helper.styleArgumentDescription(helper.argumentDescription(argument)),
|
||||
);
|
||||
});
|
||||
output = output.concat(
|
||||
this.formatItemList('Arguments:', argumentList, helper),
|
||||
);
|
||||
|
||||
// Options
|
||||
const optionGroups = this.groupItems(
|
||||
cmd.options,
|
||||
helper.visibleOptions(cmd),
|
||||
(option) => option.helpGroupHeading ?? 'Options:',
|
||||
);
|
||||
optionGroups.forEach((options, group) => {
|
||||
const optionList = options.map((option) => {
|
||||
return callFormatItem(
|
||||
helper.styleOptionTerm(helper.optionTerm(option)),
|
||||
helper.styleOptionDescription(helper.optionDescription(option)),
|
||||
);
|
||||
});
|
||||
output = output.concat(this.formatItemList(group, optionList, helper));
|
||||
});
|
||||
|
||||
if (helper.showGlobalOptions) {
|
||||
const globalOptionList = helper
|
||||
.visibleGlobalOptions(cmd)
|
||||
.map((option) => {
|
||||
return callFormatItem(
|
||||
helper.styleOptionTerm(helper.optionTerm(option)),
|
||||
helper.styleOptionDescription(helper.optionDescription(option)),
|
||||
);
|
||||
});
|
||||
output = output.concat(
|
||||
this.formatItemList('Global Options:', globalOptionList, helper),
|
||||
);
|
||||
}
|
||||
|
||||
// Commands
|
||||
const commandGroups = this.groupItems(
|
||||
cmd.commands,
|
||||
helper.visibleCommands(cmd),
|
||||
(sub) => sub.helpGroup() || 'Commands:',
|
||||
);
|
||||
commandGroups.forEach((commands, group) => {
|
||||
const commandList = commands.map((sub) => {
|
||||
return callFormatItem(
|
||||
helper.styleSubcommandTerm(helper.subcommandTerm(sub)),
|
||||
helper.styleSubcommandDescription(helper.subcommandDescription(sub)),
|
||||
);
|
||||
});
|
||||
output = output.concat(this.formatItemList(group, commandList, helper));
|
||||
});
|
||||
|
||||
return output.join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Return display width of string, ignoring ANSI escape sequences. Used in padding and wrapping calculations.
|
||||
*
|
||||
* @param {string} str
|
||||
* @returns {number}
|
||||
*/
|
||||
displayWidth(str) {
|
||||
return stripColor(str).length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Style the title for displaying in the help. Called with 'Usage:', 'Options:', etc.
|
||||
*
|
||||
* @param {string} str
|
||||
* @returns {string}
|
||||
*/
|
||||
styleTitle(str) {
|
||||
return str;
|
||||
}
|
||||
|
||||
styleUsage(str) {
|
||||
// Usage has lots of parts the user might like to color separately! Assume default usage string which is formed like:
|
||||
// command subcommand [options] [command] <foo> [bar]
|
||||
return str
|
||||
.split(' ')
|
||||
.map((word) => {
|
||||
if (word === '[options]') return this.styleOptionText(word);
|
||||
if (word === '[command]') return this.styleSubcommandText(word);
|
||||
if (word[0] === '[' || word[0] === '<')
|
||||
return this.styleArgumentText(word);
|
||||
return this.styleCommandText(word); // Restrict to initial words?
|
||||
})
|
||||
.join(' ');
|
||||
}
|
||||
styleCommandDescription(str) {
|
||||
return this.styleDescriptionText(str);
|
||||
}
|
||||
styleOptionDescription(str) {
|
||||
return this.styleDescriptionText(str);
|
||||
}
|
||||
styleSubcommandDescription(str) {
|
||||
return this.styleDescriptionText(str);
|
||||
}
|
||||
styleArgumentDescription(str) {
|
||||
return this.styleDescriptionText(str);
|
||||
}
|
||||
styleDescriptionText(str) {
|
||||
return str;
|
||||
}
|
||||
styleOptionTerm(str) {
|
||||
return this.styleOptionText(str);
|
||||
}
|
||||
styleSubcommandTerm(str) {
|
||||
// This is very like usage with lots of parts! Assume default string which is formed like:
|
||||
// subcommand [options] <foo> [bar]
|
||||
return str
|
||||
.split(' ')
|
||||
.map((word) => {
|
||||
if (word === '[options]') return this.styleOptionText(word);
|
||||
if (word[0] === '[' || word[0] === '<')
|
||||
return this.styleArgumentText(word);
|
||||
return this.styleSubcommandText(word); // Restrict to initial words?
|
||||
})
|
||||
.join(' ');
|
||||
}
|
||||
styleArgumentTerm(str) {
|
||||
return this.styleArgumentText(str);
|
||||
}
|
||||
styleOptionText(str) {
|
||||
return str;
|
||||
}
|
||||
styleArgumentText(str) {
|
||||
return str;
|
||||
}
|
||||
styleSubcommandText(str) {
|
||||
return str;
|
||||
}
|
||||
styleCommandText(str) {
|
||||
return str;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the pad width from the maximum term length.
|
||||
*
|
||||
* @param {Command} cmd
|
||||
* @param {Help} helper
|
||||
* @returns {number}
|
||||
*/
|
||||
|
||||
padWidth(cmd, helper) {
|
||||
return Math.max(
|
||||
helper.longestOptionTermLength(cmd, helper),
|
||||
helper.longestGlobalOptionTermLength(cmd, helper),
|
||||
helper.longestSubcommandTermLength(cmd, helper),
|
||||
helper.longestArgumentTermLength(cmd, helper),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect manually wrapped and indented strings by checking for line break followed by whitespace.
|
||||
*
|
||||
* @param {string} str
|
||||
* @returns {boolean}
|
||||
*/
|
||||
preformatted(str) {
|
||||
return /\n[^\S\r\n]/.test(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format the "item", which consists of a term and description. Pad the term and wrap the description, indenting the following lines.
|
||||
*
|
||||
* So "TTT", 5, "DDD DDDD DD DDD" might be formatted for this.helpWidth=17 like so:
|
||||
* TTT DDD DDDD
|
||||
* DD DDD
|
||||
*
|
||||
* @param {string} term
|
||||
* @param {number} termWidth
|
||||
* @param {string} description
|
||||
* @param {Help} helper
|
||||
* @returns {string}
|
||||
*/
|
||||
formatItem(term, termWidth, description, helper) {
|
||||
const itemIndent = 2;
|
||||
const itemIndentStr = ' '.repeat(itemIndent);
|
||||
if (!description) return itemIndentStr + term;
|
||||
|
||||
// Pad the term out to a consistent width, so descriptions are aligned.
|
||||
const paddedTerm = term.padEnd(
|
||||
termWidth + term.length - helper.displayWidth(term),
|
||||
);
|
||||
|
||||
// Format the description.
|
||||
const spacerWidth = 2; // between term and description
|
||||
const helpWidth = this.helpWidth ?? 80; // in case prepareContext() was not called
|
||||
const remainingWidth = helpWidth - termWidth - spacerWidth - itemIndent;
|
||||
let formattedDescription;
|
||||
if (
|
||||
remainingWidth < this.minWidthToWrap ||
|
||||
helper.preformatted(description)
|
||||
) {
|
||||
formattedDescription = description;
|
||||
} else {
|
||||
const wrappedDescription = helper.boxWrap(description, remainingWidth);
|
||||
formattedDescription = wrappedDescription.replace(
|
||||
/\n/g,
|
||||
'\n' + ' '.repeat(termWidth + spacerWidth),
|
||||
);
|
||||
}
|
||||
|
||||
// Construct and overall indent.
|
||||
return (
|
||||
itemIndentStr +
|
||||
paddedTerm +
|
||||
' '.repeat(spacerWidth) +
|
||||
formattedDescription.replace(/\n/g, `\n${itemIndentStr}`)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap a string at whitespace, preserving existing line breaks.
|
||||
* Wrapping is skipped if the width is less than `minWidthToWrap`.
|
||||
*
|
||||
* @param {string} str
|
||||
* @param {number} width
|
||||
* @returns {string}
|
||||
*/
|
||||
boxWrap(str, width) {
|
||||
if (width < this.minWidthToWrap) return str;
|
||||
|
||||
const rawLines = str.split(/\r\n|\n/);
|
||||
// split up text by whitespace
|
||||
const chunkPattern = /[\s]*[^\s]+/g;
|
||||
const wrappedLines = [];
|
||||
rawLines.forEach((line) => {
|
||||
const chunks = line.match(chunkPattern);
|
||||
if (chunks === null) {
|
||||
wrappedLines.push('');
|
||||
return;
|
||||
}
|
||||
|
||||
let sumChunks = [chunks.shift()];
|
||||
let sumWidth = this.displayWidth(sumChunks[0]);
|
||||
chunks.forEach((chunk) => {
|
||||
const visibleWidth = this.displayWidth(chunk);
|
||||
// Accumulate chunks while they fit into width.
|
||||
if (sumWidth + visibleWidth <= width) {
|
||||
sumChunks.push(chunk);
|
||||
sumWidth += visibleWidth;
|
||||
return;
|
||||
}
|
||||
wrappedLines.push(sumChunks.join(''));
|
||||
|
||||
const nextChunk = chunk.trimStart(); // trim space at line break
|
||||
sumChunks = [nextChunk];
|
||||
sumWidth = this.displayWidth(nextChunk);
|
||||
});
|
||||
wrappedLines.push(sumChunks.join(''));
|
||||
});
|
||||
|
||||
return wrappedLines.join('\n');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip style ANSI escape sequences from the string. In particular, SGR (Select Graphic Rendition) codes.
|
||||
*
|
||||
* @param {string} str
|
||||
* @returns {string}
|
||||
* @package
|
||||
*/
|
||||
|
||||
function stripColor(str) {
|
||||
// eslint-disable-next-line no-control-regex
|
||||
const sgrPattern = /\x1b\[\d*(;\d*)*m/g;
|
||||
return str.replace(sgrPattern, '');
|
||||
}
|
||||
|
||||
exports.Help = Help;
|
||||
exports.stripColor = stripColor;
|
||||
380
node_modules/commander/lib/option.js
generated
vendored
Normal file
380
node_modules/commander/lib/option.js
generated
vendored
Normal file
@@ -0,0 +1,380 @@
|
||||
const { InvalidArgumentError } = require('./error.js');
|
||||
|
||||
class Option {
|
||||
/**
|
||||
* Initialize a new `Option` with the given `flags` and `description`.
|
||||
*
|
||||
* @param {string} flags
|
||||
* @param {string} [description]
|
||||
*/
|
||||
|
||||
constructor(flags, description) {
|
||||
this.flags = flags;
|
||||
this.description = description || '';
|
||||
|
||||
this.required = flags.includes('<'); // A value must be supplied when the option is specified.
|
||||
this.optional = flags.includes('['); // A value is optional when the option is specified.
|
||||
// variadic test ignores <value,...> et al which might be used to describe custom splitting of single argument
|
||||
this.variadic = /\w\.\.\.[>\]]$/.test(flags); // The option can take multiple values.
|
||||
this.mandatory = false; // The option must have a value after parsing, which usually means it must be specified on command line.
|
||||
const optionFlags = splitOptionFlags(flags);
|
||||
this.short = optionFlags.shortFlag; // May be a short flag, undefined, or even a long flag (if option has two long flags).
|
||||
this.long = optionFlags.longFlag;
|
||||
this.negate = false;
|
||||
if (this.long) {
|
||||
this.negate = this.long.startsWith('--no-');
|
||||
}
|
||||
this.defaultValue = undefined;
|
||||
this.defaultValueDescription = undefined;
|
||||
this.presetArg = undefined;
|
||||
this.envVar = undefined;
|
||||
this.parseArg = undefined;
|
||||
this.hidden = false;
|
||||
this.argChoices = undefined;
|
||||
this.conflictsWith = [];
|
||||
this.implied = undefined;
|
||||
this.helpGroupHeading = undefined; // soft initialised when option added to command
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the default value, and optionally supply the description to be displayed in the help.
|
||||
*
|
||||
* @param {*} value
|
||||
* @param {string} [description]
|
||||
* @return {Option}
|
||||
*/
|
||||
|
||||
default(value, description) {
|
||||
this.defaultValue = value;
|
||||
this.defaultValueDescription = description;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Preset to use when option used without option-argument, especially optional but also boolean and negated.
|
||||
* The custom processing (parseArg) is called.
|
||||
*
|
||||
* @example
|
||||
* new Option('--color').default('GREYSCALE').preset('RGB');
|
||||
* new Option('--donate [amount]').preset('20').argParser(parseFloat);
|
||||
*
|
||||
* @param {*} arg
|
||||
* @return {Option}
|
||||
*/
|
||||
|
||||
preset(arg) {
|
||||
this.presetArg = arg;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add option name(s) that conflict with this option.
|
||||
* An error will be displayed if conflicting options are found during parsing.
|
||||
*
|
||||
* @example
|
||||
* new Option('--rgb').conflicts('cmyk');
|
||||
* new Option('--js').conflicts(['ts', 'jsx']);
|
||||
*
|
||||
* @param {(string | string[])} names
|
||||
* @return {Option}
|
||||
*/
|
||||
|
||||
conflicts(names) {
|
||||
this.conflictsWith = this.conflictsWith.concat(names);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify implied option values for when this option is set and the implied options are not.
|
||||
*
|
||||
* The custom processing (parseArg) is not called on the implied values.
|
||||
*
|
||||
* @example
|
||||
* program
|
||||
* .addOption(new Option('--log', 'write logging information to file'))
|
||||
* .addOption(new Option('--trace', 'log extra details').implies({ log: 'trace.txt' }));
|
||||
*
|
||||
* @param {object} impliedOptionValues
|
||||
* @return {Option}
|
||||
*/
|
||||
implies(impliedOptionValues) {
|
||||
let newImplied = impliedOptionValues;
|
||||
if (typeof impliedOptionValues === 'string') {
|
||||
// string is not documented, but easy mistake and we can do what user probably intended.
|
||||
newImplied = { [impliedOptionValues]: true };
|
||||
}
|
||||
this.implied = Object.assign(this.implied || {}, newImplied);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set environment variable to check for option value.
|
||||
*
|
||||
* An environment variable is only used if when processed the current option value is
|
||||
* undefined, or the source of the current value is 'default' or 'config' or 'env'.
|
||||
*
|
||||
* @param {string} name
|
||||
* @return {Option}
|
||||
*/
|
||||
|
||||
env(name) {
|
||||
this.envVar = name;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the custom handler for processing CLI option arguments into option values.
|
||||
*
|
||||
* @param {Function} [fn]
|
||||
* @return {Option}
|
||||
*/
|
||||
|
||||
argParser(fn) {
|
||||
this.parseArg = fn;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the option is mandatory and must have a value after parsing.
|
||||
*
|
||||
* @param {boolean} [mandatory=true]
|
||||
* @return {Option}
|
||||
*/
|
||||
|
||||
makeOptionMandatory(mandatory = true) {
|
||||
this.mandatory = !!mandatory;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide option in help.
|
||||
*
|
||||
* @param {boolean} [hide=true]
|
||||
* @return {Option}
|
||||
*/
|
||||
|
||||
hideHelp(hide = true) {
|
||||
this.hidden = !!hide;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @package
|
||||
*/
|
||||
|
||||
_collectValue(value, previous) {
|
||||
if (previous === this.defaultValue || !Array.isArray(previous)) {
|
||||
return [value];
|
||||
}
|
||||
|
||||
previous.push(value);
|
||||
return previous;
|
||||
}
|
||||
|
||||
/**
|
||||
* Only allow option value to be one of choices.
|
||||
*
|
||||
* @param {string[]} values
|
||||
* @return {Option}
|
||||
*/
|
||||
|
||||
choices(values) {
|
||||
this.argChoices = values.slice();
|
||||
this.parseArg = (arg, previous) => {
|
||||
if (!this.argChoices.includes(arg)) {
|
||||
throw new InvalidArgumentError(
|
||||
`Allowed choices are ${this.argChoices.join(', ')}.`,
|
||||
);
|
||||
}
|
||||
if (this.variadic) {
|
||||
return this._collectValue(arg, previous);
|
||||
}
|
||||
return arg;
|
||||
};
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return option name.
|
||||
*
|
||||
* @return {string}
|
||||
*/
|
||||
|
||||
name() {
|
||||
if (this.long) {
|
||||
return this.long.replace(/^--/, '');
|
||||
}
|
||||
return this.short.replace(/^-/, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Return option name, in a camelcase format that can be used
|
||||
* as an object attribute key.
|
||||
*
|
||||
* @return {string}
|
||||
*/
|
||||
|
||||
attributeName() {
|
||||
if (this.negate) {
|
||||
return camelcase(this.name().replace(/^no-/, ''));
|
||||
}
|
||||
return camelcase(this.name());
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the help group heading.
|
||||
*
|
||||
* @param {string} heading
|
||||
* @return {Option}
|
||||
*/
|
||||
helpGroup(heading) {
|
||||
this.helpGroupHeading = heading;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if `arg` matches the short or long flag.
|
||||
*
|
||||
* @param {string} arg
|
||||
* @return {boolean}
|
||||
* @package
|
||||
*/
|
||||
|
||||
is(arg) {
|
||||
return this.short === arg || this.long === arg;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return whether a boolean option.
|
||||
*
|
||||
* Options are one of boolean, negated, required argument, or optional argument.
|
||||
*
|
||||
* @return {boolean}
|
||||
* @package
|
||||
*/
|
||||
|
||||
isBoolean() {
|
||||
return !this.required && !this.optional && !this.negate;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This class is to make it easier to work with dual options, without changing the existing
|
||||
* implementation. We support separate dual options for separate positive and negative options,
|
||||
* like `--build` and `--no-build`, which share a single option value. This works nicely for some
|
||||
* use cases, but is tricky for others where we want separate behaviours despite
|
||||
* the single shared option value.
|
||||
*/
|
||||
class DualOptions {
|
||||
/**
|
||||
* @param {Option[]} options
|
||||
*/
|
||||
constructor(options) {
|
||||
this.positiveOptions = new Map();
|
||||
this.negativeOptions = new Map();
|
||||
this.dualOptions = new Set();
|
||||
options.forEach((option) => {
|
||||
if (option.negate) {
|
||||
this.negativeOptions.set(option.attributeName(), option);
|
||||
} else {
|
||||
this.positiveOptions.set(option.attributeName(), option);
|
||||
}
|
||||
});
|
||||
this.negativeOptions.forEach((value, key) => {
|
||||
if (this.positiveOptions.has(key)) {
|
||||
this.dualOptions.add(key);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Did the value come from the option, and not from possible matching dual option?
|
||||
*
|
||||
* @param {*} value
|
||||
* @param {Option} option
|
||||
* @returns {boolean}
|
||||
*/
|
||||
valueFromOption(value, option) {
|
||||
const optionKey = option.attributeName();
|
||||
if (!this.dualOptions.has(optionKey)) return true;
|
||||
|
||||
// Use the value to deduce if (probably) came from the option.
|
||||
const preset = this.negativeOptions.get(optionKey).presetArg;
|
||||
const negativeValue = preset !== undefined ? preset : false;
|
||||
return option.negate === (negativeValue === value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert string from kebab-case to camelCase.
|
||||
*
|
||||
* @param {string} str
|
||||
* @return {string}
|
||||
* @private
|
||||
*/
|
||||
|
||||
function camelcase(str) {
|
||||
return str.split('-').reduce((str, word) => {
|
||||
return str + word[0].toUpperCase() + word.slice(1);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Split the short and long flag out of something like '-m,--mixed <value>'
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
|
||||
function splitOptionFlags(flags) {
|
||||
let shortFlag;
|
||||
let longFlag;
|
||||
// short flag, single dash and single character
|
||||
const shortFlagExp = /^-[^-]$/;
|
||||
// long flag, double dash and at least one character
|
||||
const longFlagExp = /^--[^-]/;
|
||||
|
||||
const flagParts = flags.split(/[ |,]+/).concat('guard');
|
||||
// Normal is short and/or long.
|
||||
if (shortFlagExp.test(flagParts[0])) shortFlag = flagParts.shift();
|
||||
if (longFlagExp.test(flagParts[0])) longFlag = flagParts.shift();
|
||||
// Long then short. Rarely used but fine.
|
||||
if (!shortFlag && shortFlagExp.test(flagParts[0]))
|
||||
shortFlag = flagParts.shift();
|
||||
// Allow two long flags, like '--ws, --workspace'
|
||||
// This is the supported way to have a shortish option flag.
|
||||
if (!shortFlag && longFlagExp.test(flagParts[0])) {
|
||||
shortFlag = longFlag;
|
||||
longFlag = flagParts.shift();
|
||||
}
|
||||
|
||||
// Check for unprocessed flag. Fail noisily rather than silently ignore.
|
||||
if (flagParts[0].startsWith('-')) {
|
||||
const unsupportedFlag = flagParts[0];
|
||||
const baseError = `option creation failed due to '${unsupportedFlag}' in option flags '${flags}'`;
|
||||
if (/^-[^-][^-]/.test(unsupportedFlag))
|
||||
throw new Error(
|
||||
`${baseError}
|
||||
- a short flag is a single dash and a single character
|
||||
- either use a single dash and a single character (for a short flag)
|
||||
- or use a double dash for a long option (and can have two, like '--ws, --workspace')`,
|
||||
);
|
||||
if (shortFlagExp.test(unsupportedFlag))
|
||||
throw new Error(`${baseError}
|
||||
- too many short flags`);
|
||||
if (longFlagExp.test(unsupportedFlag))
|
||||
throw new Error(`${baseError}
|
||||
- too many long flags`);
|
||||
|
||||
throw new Error(`${baseError}
|
||||
- unrecognised flag format`);
|
||||
}
|
||||
if (shortFlag === undefined && longFlag === undefined)
|
||||
throw new Error(
|
||||
`option creation failed due to no flags found in '${flags}'.`,
|
||||
);
|
||||
|
||||
return { shortFlag, longFlag };
|
||||
}
|
||||
|
||||
exports.Option = Option;
|
||||
exports.DualOptions = DualOptions;
|
||||
101
node_modules/commander/lib/suggestSimilar.js
generated
vendored
Normal file
101
node_modules/commander/lib/suggestSimilar.js
generated
vendored
Normal file
@@ -0,0 +1,101 @@
|
||||
const maxDistance = 3;
|
||||
|
||||
function editDistance(a, b) {
|
||||
// https://en.wikipedia.org/wiki/Damerau–Levenshtein_distance
|
||||
// Calculating optimal string alignment distance, no substring is edited more than once.
|
||||
// (Simple implementation.)
|
||||
|
||||
// Quick early exit, return worst case.
|
||||
if (Math.abs(a.length - b.length) > maxDistance)
|
||||
return Math.max(a.length, b.length);
|
||||
|
||||
// distance between prefix substrings of a and b
|
||||
const d = [];
|
||||
|
||||
// pure deletions turn a into empty string
|
||||
for (let i = 0; i <= a.length; i++) {
|
||||
d[i] = [i];
|
||||
}
|
||||
// pure insertions turn empty string into b
|
||||
for (let j = 0; j <= b.length; j++) {
|
||||
d[0][j] = j;
|
||||
}
|
||||
|
||||
// fill matrix
|
||||
for (let j = 1; j <= b.length; j++) {
|
||||
for (let i = 1; i <= a.length; i++) {
|
||||
let cost = 1;
|
||||
if (a[i - 1] === b[j - 1]) {
|
||||
cost = 0;
|
||||
} else {
|
||||
cost = 1;
|
||||
}
|
||||
d[i][j] = Math.min(
|
||||
d[i - 1][j] + 1, // deletion
|
||||
d[i][j - 1] + 1, // insertion
|
||||
d[i - 1][j - 1] + cost, // substitution
|
||||
);
|
||||
// transposition
|
||||
if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) {
|
||||
d[i][j] = Math.min(d[i][j], d[i - 2][j - 2] + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return d[a.length][b.length];
|
||||
}
|
||||
|
||||
/**
|
||||
* Find close matches, restricted to same number of edits.
|
||||
*
|
||||
* @param {string} word
|
||||
* @param {string[]} candidates
|
||||
* @returns {string}
|
||||
*/
|
||||
|
||||
function suggestSimilar(word, candidates) {
|
||||
if (!candidates || candidates.length === 0) return '';
|
||||
// remove possible duplicates
|
||||
candidates = Array.from(new Set(candidates));
|
||||
|
||||
const searchingOptions = word.startsWith('--');
|
||||
if (searchingOptions) {
|
||||
word = word.slice(2);
|
||||
candidates = candidates.map((candidate) => candidate.slice(2));
|
||||
}
|
||||
|
||||
let similar = [];
|
||||
let bestDistance = maxDistance;
|
||||
const minSimilarity = 0.4;
|
||||
candidates.forEach((candidate) => {
|
||||
if (candidate.length <= 1) return; // no one character guesses
|
||||
|
||||
const distance = editDistance(word, candidate);
|
||||
const length = Math.max(word.length, candidate.length);
|
||||
const similarity = (length - distance) / length;
|
||||
if (similarity > minSimilarity) {
|
||||
if (distance < bestDistance) {
|
||||
// better edit distance, throw away previous worse matches
|
||||
bestDistance = distance;
|
||||
similar = [candidate];
|
||||
} else if (distance === bestDistance) {
|
||||
similar.push(candidate);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
similar.sort((a, b) => a.localeCompare(b));
|
||||
if (searchingOptions) {
|
||||
similar = similar.map((candidate) => `--${candidate}`);
|
||||
}
|
||||
|
||||
if (similar.length > 1) {
|
||||
return `\n(Did you mean one of ${similar.join(', ')}?)`;
|
||||
}
|
||||
if (similar.length === 1) {
|
||||
return `\n(Did you mean ${similar[0]}?)`;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
exports.suggestSimilar = suggestSimilar;
|
||||
Reference in New Issue
Block a user