@fontoxml/fontoxml-development-tools
Version:
Development tools for Fonto.
176 lines (157 loc) • 5.03 kB
JavaScript
import InputError from './InputError.js';
import symbols from './symbols.js';
/**
* @param {Command} root
* @param {String|Array<String>} [parts]
* @return {Array<[]>}
*/
function matchInputToSyntaxPaths(root, parts) {
if (!parts) {
parts = [];
}
if (typeof parts === 'string') {
parts = parts
.match(/(".*?"|[^"\s]+)+(?=\s*|\s*$)/g)
.map((str) => str.replace(/['"]+/g, ''));
}
// Tiers are two collections of syntax parts; ordered ones need to be written in order by the user, unordered syntax
// parts may occur at any point in a command line input
let tiers = {
// Commands, parameters
ordered: [root],
// Options
unordered: [],
};
//
root[symbols.updateTiersAfterMatch](tiers, root);
// Match and validate syntax parts based on input
let resolvedInputSpecs = [
{
// The root command represents having invoked ask-nicely in the first place,
// or in other words, it's what happens if you were to run your script without any input
syntax: root,
input: root,
isUndefined: false,
},
];
while (parts.length) {
const expectedScopes = [...tiers.unordered].concat(tiers.ordered);
const matchingScope = expectedScopes.find((scope) =>
scope[symbols.isMatchForPart](parts[0])
);
if (!matchingScope) {
throw new InputError(
`The input "${parts[0]}" was not expected`,
null,
'EINVAL'
);
}
// Allow the SyntaxPart to modify the string that is being evaluated
// eg. allow an Option to change "-abc" to "-bc" for following SyntaxParts
// @DEBT returns a modified matchingScope _and_ changes by reference `parts`
const matchingValue =
matchingScope[symbols.spliceInputFromParts](parts, tiers);
// Update the collection of parsed values (each coupled to their matching SyntaxPart)
resolvedInputSpecs = matchingScope[symbols.updateInputSpecsAfterMatch](
resolvedInputSpecs,
matchingValue
);
// Update the tiers (ordered/unordered) for whatever is left to parse
tiers = matchingScope[symbols.updateTiersAfterMatch](
tiers,
matchingValue
);
}
// Find everything that is still open to match, and map it to the same format as resolvedScopeValues
// This information is needed so that parts (like an --option) can set itself to a default conditionally of being
// used, or not
const unresolvedInputSpecs = [...tiers.ordered, ...tiers.unordered]
.reduce((leftovers, tierOptions) => leftovers.concat(tierOptions), [])
.filter(
(syntaxPart) =>
!resolvedInputSpecs.find((match) => match.syntax === syntaxPart)
)
.map((unmatch) => ({
syntax: unmatch,
input: undefined,
isUndefined: true,
}));
return [resolvedInputSpecs, unresolvedInputSpecs];
}
/**
*
* @param {Request} request
* @param {Array<[]>} inputSpecs
* @param {Array<*>} rest
* @return {Promise}
*/
export default function interpreter(root, parts, request, ...rest) {
// Reset state on the request to clear any data from previous interperter calls.
request.command = null;
request.options = {};
request.parameters = {};
const [resolvedInputSpecs, unresolvedInputSpecs] = matchInputToSyntaxPaths(
root,
parts
);
return (
[
...resolvedInputSpecs.map((valueSpec) => {
// Maybe set the default
valueSpec.input = valueSpec.syntax[symbols.applyDefault](
valueSpec.input,
!!valueSpec.isUndefined
);
valueSpec.syntax[symbols.validateInput](valueSpec.input);
return valueSpec;
}),
...unresolvedInputSpecs
.map((valueSpec) => {
// Maybe set the default
valueSpec.input = valueSpec.syntax[symbols.applyDefault](
valueSpec.input,
!!valueSpec.isUndefined
);
valueSpec.syntax[symbols.validateInput](valueSpec.input);
return valueSpec;
})
.reverse(),
]
// Resolve the valueSpecs in sequence with each given the output of their predecessor
.reduce(async (asyncLast, valueSpec) => {
// Run the Command#addResolver configuration
if (valueSpec.syntax.resolver) {
valueSpec.input = await valueSpec.syntax.resolver(
valueSpec.input,
...rest
);
}
// Run the Command#addValidator configuration
await valueSpec.syntax.validateValue(valueSpec.input);
const mergedRequestObject = await asyncLast;
// This is the object a syntax part generated, to be spliced into the Request object
// eg. { options: { foo: 'bar' }}
const contribution =
(await valueSpec.syntax[
symbols.createContributionToRequestObject
](
mergedRequestObject,
valueSpec.input,
!!valueSpec.isUndefined
)) || {};
// Merge the contribution into the original Request object
// eg. { command: Command, options: { foo: 'bar', 'boo': 'baz' }}
return Object.keys(contribution).reduce((merged, key) => {
if (key === 'command') {
merged[key] = contribution[key];
} else {
merged[key] = Object.assign(
merged[key] || {},
contribution[key]
);
}
return merged;
}, mergedRequestObject);
}, request || {})
);
}