buncha stuff
This commit is contained in:
parent
a0e89367dd
commit
41b23ccdef
102
src/Parser.ts
102
src/Parser.ts
@ -1,5 +1,9 @@
|
||||
import { EventEmitter } from 'events';
|
||||
import CommandOption from './classes/CommandOption';
|
||||
|
||||
import ICommand from "./interfaces/Command";
|
||||
import ICommandOption, { OptionType } from "./interfaces/CommandOption";
|
||||
import { OptionType } from "./interfaces/CommandOption";
|
||||
import IResolver from './interfaces/Resolver';
|
||||
import ExtendedMap from "./util/Map";
|
||||
import Util from "./util/Util";
|
||||
|
||||
@ -9,18 +13,33 @@ type ParseResult = {
|
||||
subcommandGroup: string | null
|
||||
}
|
||||
|
||||
type ParserOptions = {
|
||||
commands: Iterable<ICommand>,
|
||||
prefix?: string,
|
||||
debug?: boolean,
|
||||
resolver: IResolver<unknown, unknown, unknown, unknown>
|
||||
}
|
||||
|
||||
const flagReg = /(?:^| )(?<flag>(?:--[a-z0-9]{3,})|(?:-[a-z]{1,2}))(?:$| )/iu;
|
||||
|
||||
class Parser {
|
||||
class Parser extends EventEmitter {
|
||||
|
||||
private commands: ExtendedMap<string, ICommand>;
|
||||
|
||||
private _debug = false;
|
||||
|
||||
prefix: string;
|
||||
|
||||
constructor(commands: Iterable<ICommand>, prefix?: string) {
|
||||
resolver: IResolver<unknown, unknown, unknown, unknown>;
|
||||
|
||||
constructor({ commands, prefix, debug = false, resolver }: ParserOptions) {
|
||||
|
||||
super();
|
||||
|
||||
this._debug = debug;
|
||||
this.commands = new ExtendedMap<string, ICommand>();
|
||||
for (const command of commands) this.commands.set(command.name, command);
|
||||
this.resolver = resolver;
|
||||
|
||||
this.prefix = prefix || '';
|
||||
|
||||
@ -33,26 +52,26 @@ class Parser {
|
||||
return command || null;
|
||||
}
|
||||
|
||||
async parseMessage(message: string) {
|
||||
async parseMessage(message: string, prefix = this.prefix) {
|
||||
|
||||
if (!message.startsWith(this.prefix) || !message.length) return null;
|
||||
let params: string[] = message.replace(this.prefix, '').split(' ').filter((str) => str.length);
|
||||
if (!message.startsWith(prefix) || !message.length) return null;
|
||||
let params: string[] = message.replace(prefix, '').split(' ').filter((str) => str.length);
|
||||
const [commandName] = params.shift() || [];
|
||||
if (!commandName) return null;
|
||||
|
||||
const command = this.matchCommand(commandName);
|
||||
if (!command) return null;
|
||||
|
||||
// const { command, target: message, guild } = invoker;
|
||||
this.debug(`Matched command ${command.name}`);
|
||||
const { subcommands, subcommandGroups } = command;
|
||||
const args: {[key: string]: ICommandOption} = {};
|
||||
const args: {[key: string]: CommandOption} = {};
|
||||
const parseResult: ParseResult = { args, subcommand: null, subcommandGroup: null };
|
||||
// console.log(options);
|
||||
let group = null,
|
||||
subcommand = null;
|
||||
|
||||
// Parse out subcommands
|
||||
if (subcommandGroups.length || subcommands.length) {
|
||||
this.debug(`Expecting subcommand`);
|
||||
const [first, second, ...rest] = params;
|
||||
group = command.subcommandGroup(first);
|
||||
|
||||
@ -69,7 +88,8 @@ class Parser {
|
||||
parseResult.subcommand = subcommand?.name || null;
|
||||
parseResult.subcommandGroup = group?.name || null;
|
||||
|
||||
if (!subcommand) return { showUsage: true, verbose: true };
|
||||
if (!subcommand) throw new Error(`Expecting subcommand, one of ${subcommands.map((s) => s.name)}`); //return { showUsage: true, verbose: true };
|
||||
this.debug(`Got ${subcommand.name}`);
|
||||
|
||||
}
|
||||
|
||||
@ -77,17 +97,16 @@ class Parser {
|
||||
const flags = activeCommand.options.filter((opt) => opt.flag);
|
||||
|
||||
params = Util.parseQuotes(params.join(' ')).map(([str]: (string | boolean)[]): string => str.toString());
|
||||
this.debug(`Given params: "${params.join('", "')}"`);
|
||||
let currentFlag = null;
|
||||
// console.log('params', params);
|
||||
|
||||
// Parse flags
|
||||
for (let index = 0; index < params.length;) {
|
||||
|
||||
// console.log(params[index]);
|
||||
const match = flagReg.exec(params[index]);
|
||||
if (!match || !match.groups) {
|
||||
// console.log('no match', currentFlag?.name);
|
||||
if (currentFlag) { // Add potential value resolveables to the flag's raw value until next flag is hit, if there is one
|
||||
this.debug(`Appending value to ${currentFlag.name}: ${params[index]}`);
|
||||
if (currentFlag.plural) { // The parse function only parses consecutive values
|
||||
if (!currentFlag.rawValue) currentFlag.rawValue = [];
|
||||
currentFlag.rawValue.push(params[index]);
|
||||
@ -100,16 +119,15 @@ class Parser {
|
||||
continue;
|
||||
}
|
||||
|
||||
// console.log('matched');
|
||||
const _flag = match.groups.flag.replace(/--?/u, '').toLowerCase();
|
||||
let aliased = false;
|
||||
const flag = flags.find((f) => {
|
||||
aliased = f.valueAsAlias && f.choices.some((c) => c === _flag);
|
||||
return f.name === _flag || aliased;
|
||||
});
|
||||
if (!flag) throw new Error(`Unrecognised flag: ${_flag}`); //{ error: true, index: 'O_COMMANDHANDLER_UNRECOGNISED_FLAG', params: { flag: _flag } };
|
||||
if (!flag) throw new Error(`Unrecognised flag: ${_flag}`);
|
||||
this.debug(`Matched flag: ${flag.name} with ${_flag}`);
|
||||
|
||||
// console.log('aliased', aliased);
|
||||
params.splice(index, 1, '');
|
||||
if (aliased) {
|
||||
(args[flag.name] = flag.clone([_flag])).aliased = true;
|
||||
@ -119,42 +137,44 @@ class Parser {
|
||||
args[flag.name] = currentFlag;
|
||||
}
|
||||
index++;
|
||||
// console.log('------------------------------');
|
||||
|
||||
}
|
||||
|
||||
// Clean up params for option parsing
|
||||
for (const flag of Object.values(args)) {
|
||||
// console.log('flags loop', flag.name, flag._rawValue);
|
||||
this.debug(`Running parser for ${flag.name}`);
|
||||
const result = await flag.parse();
|
||||
if (result.error) {
|
||||
if (flag.choices.length) {
|
||||
return { error: true, index: 'O_COMMANDHANDLER_INVALID_CHOICE', params: { option: flag.name, value: flag.rawValue, choices: flag.choices.map((c) => c).join('`, `') } };
|
||||
throw new Error(`Invalid choice for ${flag.name}, Valid choices are ${flag.choices.join(', ')}.`);
|
||||
}
|
||||
return { option: flag, ...result.removed };
|
||||
throw new Error(`Failed to parse value for ${flag.name}, expected value type: ${flag.type}`); //return { option: flag, ...result.removed };
|
||||
}
|
||||
this.debug(`Cleaning up params after ${flag.name}`);
|
||||
for (const r of result.removed) params.splice(params.indexOf(r), 1);
|
||||
}
|
||||
|
||||
// console.log('params', params);
|
||||
this.debug(`Params after parsing "${params.join('", "')}"`);
|
||||
const options = activeCommand.options.filter((opt) => !opt.flag && (opt.type !== OptionType.STRING || opt.choices.length));
|
||||
// const choiceOpts = activeCommand.options.filter((opt) => opt.choices.length);
|
||||
const stringOpts = activeCommand.options.filter((opt) => !opt.flag && !opt.choices.length && opt.type === OptionType.STRING);
|
||||
// console.log('non-flag options', options.map((opt) => opt.name));
|
||||
|
||||
this.debug(`Parsing non-flag options`);
|
||||
// Parse out non-flag options
|
||||
for (const option of options) { // String options are parsed separately at the end
|
||||
// console.log(1, params);
|
||||
if (!params.some((param) => param !== null)) break;
|
||||
// console.log(2);
|
||||
if (!params.some((param) => param !== null)) {
|
||||
this.debug(`No potential values left in params`);
|
||||
break;
|
||||
}
|
||||
|
||||
this.debug(`Trying ${option.name}, plural: ${option.plural}`);
|
||||
|
||||
const cloned = option.clone();
|
||||
let removed: string[] = [],
|
||||
error = false;
|
||||
if (cloned.plural) { // E.g. if the type is CHANNEL**S**, parse out any potential channels from the message
|
||||
// console.log('plural');
|
||||
cloned.rawValue = params;
|
||||
({ removed } = await cloned.parse());
|
||||
} else for (let index = 0; index < params.length;) { // Attempt to parse out a value from each param
|
||||
// console.log('singular');
|
||||
if (params[index] === null) {
|
||||
index++;
|
||||
continue;
|
||||
@ -164,7 +184,10 @@ class Parser {
|
||||
if (!error) break;
|
||||
index++;
|
||||
}
|
||||
if (error) continue;
|
||||
if (error) {
|
||||
this.debug(`Failed to parse any values for ${option.name} with params "${option.rawValue?.join('", "')}"`);
|
||||
continue;
|
||||
}
|
||||
|
||||
args[cloned.name] = cloned;
|
||||
// Clean up params for string parsing
|
||||
@ -172,18 +195,15 @@ class Parser {
|
||||
|
||||
}
|
||||
|
||||
this.debug(`Going through remaining params for string values: "${params.join('", "')}"`);
|
||||
const strings = [];
|
||||
let tmpString = '';
|
||||
// console.log('strings loop');
|
||||
// console.log(params);
|
||||
|
||||
// Compile strings into groups of strings so we don't get odd looking strings from which options have been parsed out of
|
||||
for (let index = 0; index < params.length;) {
|
||||
const str = params[index];
|
||||
// console.log(str);
|
||||
if (!str) {
|
||||
// console.log('null string');
|
||||
if (tmpString.length) {
|
||||
// console.log('pushing', tmpString);
|
||||
strings.push(tmpString);
|
||||
tmpString = '';
|
||||
}
|
||||
@ -194,24 +214,18 @@ class Parser {
|
||||
tmpString += ` ${str}`;
|
||||
tmpString = tmpString.trim();
|
||||
}
|
||||
// console.log('tmpString', tmpString);
|
||||
if (tmpString.length) strings.push(tmpString);
|
||||
|
||||
// console.log('params after', params);
|
||||
// console.log('strings', strings);
|
||||
|
||||
if (strings.length) for (const strOpt of stringOpts) {
|
||||
const val = strings.shift();
|
||||
if (!val) break;
|
||||
const cloned = strOpt.clone([val]);
|
||||
// console.log(cloned.name, cloned._rawValue);
|
||||
await cloned.parse();
|
||||
args[cloned.name] = cloned;
|
||||
}
|
||||
|
||||
// This part is obsolete now, I think, the string option checks the choice value
|
||||
for (const arg of Object.values(args)) {
|
||||
// console.log(arg.name, arg.value);
|
||||
if (!arg.choices.length) continue;
|
||||
if (!arg.choices.some((choice) => {
|
||||
if (typeof arg.value === 'string' && typeof choice === 'string') return arg.value.toLowerCase() === choice.toLowerCase();
|
||||
@ -219,17 +233,21 @@ class Parser {
|
||||
})) throw new Error(`Invalid choice: ${arg.name} value must be one of ${arg.choices.join(', ')}`); //return { error: true, index: 'O_COMMANDHANDLER_INVALID_CHOICE', params: { option: arg.name, value: arg.value, choices: arg.choices.map((c) => c).join('`, `') } };
|
||||
}
|
||||
|
||||
this.debug(`Making sure required options were given.`);
|
||||
for (const req of activeCommand.options.filter((opt) => opt.required))
|
||||
if (!args[req.name])
|
||||
throw new Error(`${req.name} is a required option`);
|
||||
//return { option: req, error: true, required: true };
|
||||
|
||||
// console.log('parsed args final', Object.values(args).map((arg) => `\n${arg.name}: ${arg.value}, ${inspect(arg._rawValue)}`).join(''));
|
||||
if (strings.length) throw new Error(`Unrecognised option(s): "${strings.join('", "')}"`);//return { error: true, index: 'O_COMMANDHANDLER_UNRECOGNISED_OPTIONS', params: { opts: strings.join('`, `') } };
|
||||
|
||||
return { options: args, verbose: true };
|
||||
|
||||
}
|
||||
|
||||
private debug(message: string) {
|
||||
if(this._debug) this.emit('debug', `[PARSER] ${message}`);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export { Parser };
|
@ -1,7 +1,8 @@
|
||||
import { ICommandOption, OptionType } from "../interfaces/CommandOption";
|
||||
import { OptionType } from "../interfaces/CommandOption";
|
||||
import { ICommand, CommandDefinition } from "../interfaces/Command";
|
||||
import SubcommandOption from "./SubcommandOption";
|
||||
import SubcommandGroupOption from "./SubcommandGroupOption";
|
||||
import CommandOption from "./CommandOption";
|
||||
|
||||
class Command implements ICommand {
|
||||
|
||||
@ -9,7 +10,7 @@ class Command implements ICommand {
|
||||
|
||||
aliases: string[];
|
||||
|
||||
options: ICommandOption[];
|
||||
options: CommandOption[];
|
||||
|
||||
constructor(def: CommandDefinition) {
|
||||
|
||||
@ -39,7 +40,7 @@ class Command implements ICommand {
|
||||
return this.subcommandGroups.find((opt) => opt.name === name) || null;
|
||||
}
|
||||
|
||||
private _subcommands(options: ICommandOption[]): SubcommandOption[] {
|
||||
private _subcommands(options: CommandOption[]): SubcommandOption[] {
|
||||
const subcommands: SubcommandOption[] = [];
|
||||
for (const opt of options) {
|
||||
if (opt.type === OptionType.SUB_COMMAND) subcommands.push(opt);
|
||||
|
@ -1,12 +1,16 @@
|
||||
import ICommandOption, { Choice, CommandOptionDefinition, DependsOnMode, OptionType, ParseResult } from "../interfaces/CommandOption";
|
||||
import IResolver from "../interfaces/Resolver";
|
||||
|
||||
class CommandOption implements ICommandOption {
|
||||
|
||||
[key: string]: unknown;
|
||||
|
||||
name: string;
|
||||
|
||||
aliases: string[];
|
||||
|
||||
options: ICommandOption[];
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
options: CommandOption[];
|
||||
|
||||
type: OptionType;
|
||||
|
||||
@ -36,6 +40,8 @@ class CommandOption implements ICommandOption {
|
||||
|
||||
aliased = false;
|
||||
|
||||
private resolver?: IResolver<unknown, unknown, unknown, unknown>|undefined = undefined;
|
||||
|
||||
constructor(def: CommandOptionDefinition|ICommandOption) {
|
||||
|
||||
this.name = def.name;
|
||||
@ -58,20 +64,31 @@ class CommandOption implements ICommandOption {
|
||||
|
||||
}
|
||||
|
||||
clone(rawValue?: string[]): ICommandOption {
|
||||
clone(rawValue?: string[], resolver?: IResolver<unknown, unknown, unknown, unknown>): CommandOption {
|
||||
const opt = new CommandOption(this);
|
||||
opt.rawValue = rawValue;
|
||||
opt.resolver = resolver || undefined;
|
||||
return opt;
|
||||
}
|
||||
|
||||
parse(): ParseResult {
|
||||
return { error: false, removed: [] };
|
||||
async parse(): Promise<ParseResult> {
|
||||
if(!this[OptionType[this.type]]) throw new Error(`Missing parsing function for ${this.type}`);
|
||||
const result = await this[OptionType[this.type]];
|
||||
return result as ParseResult;
|
||||
}
|
||||
|
||||
get plural(): boolean {
|
||||
return this.type.toString().endsWith('S');
|
||||
}
|
||||
|
||||
protected async MEMBER() {
|
||||
if (!this.resolver) throw new Error('Missing resolver');
|
||||
if(!this.rawValue) throw new Error('Missing raw value');
|
||||
const member = await this.resolver?.resolveMember(this.rawValue[0]);
|
||||
if (!member) return { error: true };
|
||||
return { value: member, removed: this.rawValue };
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export { CommandOption };
|
||||
|
@ -1,13 +1,13 @@
|
||||
import CommandOption from '../classes/CommandOption';
|
||||
import SubcommandGroupOption from '../classes/SubcommandGroupOption';
|
||||
import SubcommandOption from '../classes/SubcommandOption';
|
||||
import { ICommandOption } from './CommandOption';
|
||||
|
||||
|
||||
interface ICommand {
|
||||
|
||||
name: string,
|
||||
aliases: string[]
|
||||
options: ICommandOption[]
|
||||
options: CommandOption[]
|
||||
|
||||
get subcommands(): SubcommandOption[]
|
||||
|
||||
@ -22,7 +22,7 @@ interface ICommand {
|
||||
type CommandDefinition = {
|
||||
name: string;
|
||||
aliases?: string[];
|
||||
options?: ICommandOption[];
|
||||
options?: CommandOption[];
|
||||
}
|
||||
|
||||
export { ICommand, CommandDefinition };
|
||||
|
@ -28,6 +28,9 @@
|
||||
// | 'FLOAT'
|
||||
// | 'POINTS'
|
||||
|
||||
import CommandOption from "../classes/CommandOption";
|
||||
import IResolver from "./Resolver";
|
||||
|
||||
enum OptionType {
|
||||
SUB_COMMAND,
|
||||
SUB_COMMAND_GROUP,
|
||||
@ -53,10 +56,8 @@ enum OptionType {
|
||||
VOICE_CHANNEL,
|
||||
CHANNEL,
|
||||
ROLE,
|
||||
MENTIONABLE,
|
||||
NUMBER,
|
||||
FLOAT,
|
||||
POINTS
|
||||
}
|
||||
|
||||
type Choice = string | number | boolean
|
||||
@ -71,7 +72,7 @@ type CommandOptionDefinition = {
|
||||
name: string;
|
||||
aliases?: string[];
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
options?: ICommandOption[];
|
||||
options?: CommandOption[];
|
||||
type?: OptionType;
|
||||
|
||||
required?: boolean;
|
||||
@ -89,13 +90,16 @@ type CommandOptionDefinition = {
|
||||
};
|
||||
|
||||
interface ICommandOption {
|
||||
|
||||
// Allows for accessing the class properties with string indices, e.g. this['string']
|
||||
[key: string]: unknown;
|
||||
// Option name
|
||||
name: string;
|
||||
// Optional alises
|
||||
aliases: string[];
|
||||
|
||||
// Sub options
|
||||
options: ICommandOption[];
|
||||
options: CommandOption[];
|
||||
choices: Choice[]
|
||||
type: OptionType;
|
||||
|
||||
@ -117,11 +121,13 @@ interface ICommandOption {
|
||||
rawValue?: string[]
|
||||
aliased: boolean
|
||||
|
||||
// resolver?: IResolver<unknown, unknown, unknown, unknown>|undefined
|
||||
|
||||
// private _options?: CommandOptionDefinition
|
||||
|
||||
clone(rawValue?: string[]): ICommandOption
|
||||
clone(rawValue?: string[], resolver?: IResolver<unknown, unknown, unknown, unknown>): CommandOption
|
||||
|
||||
parse(): ParseResult
|
||||
parse(): Promise<ParseResult>
|
||||
|
||||
get plural(): boolean
|
||||
|
||||
|
18
src/interfaces/Resolver.ts
Normal file
18
src/interfaces/Resolver.ts
Normal file
@ -0,0 +1,18 @@
|
||||
interface IResolver <User, Member, Channel, Role> {
|
||||
|
||||
resolveUser(resolveable: string): User
|
||||
|
||||
resolveMember(resolveable: string): Member
|
||||
|
||||
resolveChannel(resolveable: string): Channel
|
||||
|
||||
resolveRole(resolveable: string): Role
|
||||
|
||||
resolveBoolean(resolveable: string): boolean
|
||||
|
||||
resolveTime(resolveable: string): number
|
||||
|
||||
}
|
||||
|
||||
export { IResolver };
|
||||
export default IResolver;
|
Loading…
Reference in New Issue
Block a user