@fontoxml/fontoxml-development-tools
Version:
Development tools for Fonto.
243 lines (210 loc) • 6 kB
JavaScript
import interpreter from './interpreter.js';
import NamedSyntaxPart from './NamedSyntaxPart.js';
import Option from './Option.js';
import Parameter from './Parameter.js';
import Request from './Request.js';
import symbols from './symbols.js';
const CHILD_CLASS = Symbol('child command class definition');
export default class Command extends NamedSyntaxPart {
constructor(name, controller) {
super(name);
this.parent = null;
this.controller = null;
this.children = [];
this.aliases = [];
this.options = [];
this.parameters = [];
this.preControllers = [];
this.setNewChildClass(Command);
this.setController(controller);
}
[symbols.isMatchForPart](value) {
return !!this.getCommandByName(value);
}
[symbols.updateTiersAfterMatch](tiers, syntaxPartThatWasMatched) {
tiers.ordered.splice(tiers.ordered.indexOf(this), 1);
if (syntaxPartThatWasMatched.getType() === 'command') {
// Remove all previously defined options of the same name
tiers.unordered
.filter((x) =>
syntaxPartThatWasMatched.options.some(
(y) => x.name === y.name
)
)
.forEach((option) =>
tiers.unordered.splice(tiers.unordered.indexOf(option), 1)
);
tiers.ordered.splice(
0,
0,
...syntaxPartThatWasMatched.parameters.concat(
syntaxPartThatWasMatched
)
);
tiers.unordered.splice(0, 0, ...syntaxPartThatWasMatched.options);
}
return tiers;
}
[symbols.spliceInputFromParts](parts, _tiers) {
return this.getCommandByName(parts.shift());
}
[symbols.createContributionToRequestObject](_accumulated, value) {
return value ? { command: value } : null;
}
/**
* Parse input onto a request object, and execute accordingly
* @param {String|Array<String>} [parts]
* @param {Object} [request] An existing Request, if you do not want to make a new one if you want to re-use it
* @return {Promise}
*/
async execute(parts, initialRequest, ...args) {
const request = await interpreter(
this,
parts,
initialRequest || new Request(),
...args
);
return request.command
.getControllerStack()
.apply(request.command, [request, ...args]);
}
getType() {
return 'command';
}
/**
*
* @param parts
* @param request
* @param args
*/
// parse (parts, request, ...args) {
// return interpreter(this, parts, request || new Request(), ...args);
// }
/**
* Execute all ancestroy precontrollers
* @param args
* @return {*}
*/
executePreControllers(...args) {
return this.preControllers.reduce(
(res, preController) =>
res.then((previousVal) =>
previousVal === false ? previousVal : preController(...args)
),
this.parent
? this.parent.executePreControllers(...args)
: Promise.resolve(true)
);
}
/**
* Returns a function that executes all ancestry precontrollers and this command's controller
* @return {function(Type, Type): Type}
*/
getControllerStack() {
return (...args) =>
this.executePreControllers(...args).then((previousValue) =>
previousValue === false || typeof this.controller !== 'function'
? previousValue
: this.controller(...args)
);
}
/**
* Look up a child command by it's name
* @param {string} name
* @return {Command|undefined}
*/
getCommandByName(name) {
return this.children.find(
(child) => child.name === name || child.aliases.indexOf(name) >= 0
);
}
/**
* Set the main controller
* @param {function(Type, Type): Type} cb
* @return {Command}
*/
setController(cb) {
this.controller = cb;
return this;
}
/**
* Defines what class new child instances should have if they're being instantiated by this object
* @param ClassObject
* @return {Command}
*/
setNewChildClass(ClassObject) {
this[CHILD_CLASS] = ClassObject;
return this;
}
/**
* Add a precontroller function that is ran before its own controller, or any of it's descendants precontrollers
* @param {function(Type, Type): Type} cb
* @return {Command}
*/
addPreController(cb) {
this.preControllers.push(cb);
return this;
}
/**
* Give command an alternative name
* @param {string} name
* @return {Command}
*/
addAlias(name) {
this.aliases.push(name);
return this;
}
/**
* Describe an option
* @param {Option|string} long - The identifying name of this option, unique for its ancestry
* @param {string} [short] - A one-character alias of this option, unique for its ancestry
* @param {string} [description]
* @param {boolean} [required] - If true, an omittance would throw an error
* @return {Command}
*/
addOption(long, short, description, required) {
this.options.push(
typeof long.getType === 'function' && long.getType() === 'option'
? long
: new Option(long)
.setShort(short)
.setDescription(description)
.isRequired(required)
);
return this;
}
/**
* Describes a parameter. Notice tat if a command has child commands, *required is implied for all ancestor parameters
* (and child cmd names will be mistaken for parameters if some is missing)
* @param {Parameter|string} name
* @param {string} [description]
* @param {boolean} [required]
* @return {Command}
*/
addParameter(name, description, required) {
this.parameters.push(
typeof name.getType === 'function' && name.getType() === 'parameter'
? name
: new Parameter(name)
.setDescription(description)
.isRequired(required)
);
return this;
}
/**
* Register a command as a child of this, and register this as parent of the child
* @TODO: Check if child is not in lineage of command, to avoid circularness
* @param {string|Command} name
* @param {function(Type, Type): Type} [controller]
* @return {Command} The child command
*/
addCommand(name, controller) {
const child =
typeof name.getType === 'function' && name.getType() === 'command'
? name
: new this[CHILD_CLASS](name, controller);
child.parent = this;
this.children.push(child);
return child;
}
}