1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
|
import { createInterface, Interface } from 'readline';
import { red, green, white } from 'colors';
export interface Configuration {
identifier: () => string | string;
onInvalid?: (command: string, validCommand: boolean) => string | string;
onValid?: (success?: string) => string | string;
isCaseSensitive?: boolean;
}
export type ReplAction = (parsedArgs: Array<string>) => any | Promise<any>;
export interface Registration {
argPatterns: RegExp[];
action: ReplAction;
}
export default class Repl {
private identifier: () => string | string;
private onInvalid: ((command: string, validCommand: boolean) => string) | string;
private onValid: ((success: string) => string) | string;
private isCaseSensitive: boolean;
private commandMap = new Map<string, Registration[]>();
public interface: Interface;
private busy = false;
private keys: string | undefined;
constructor({ identifier: prompt, onInvalid, onValid, isCaseSensitive }: Configuration) {
this.identifier = prompt;
this.onInvalid = onInvalid || this.usage;
this.onValid = onValid || this.success;
this.isCaseSensitive = isCaseSensitive ?? true;
this.interface = createInterface(process.stdin, process.stdout).on('line', this.considerInput);
}
private resolvedIdentifier = () => (typeof this.identifier === 'string' ? this.identifier : this.identifier());
private usage = (command: string, validCommand: boolean) => {
if (validCommand) {
const formatted = white(command);
const patterns = green(
this.commandMap
.get(command)!
.map(({ argPatterns }) => `${formatted} ${argPatterns.join(' ')}`)
.join('\n')
);
return `${this.resolvedIdentifier()}\nthe given arguments do not match any registered patterns for ${formatted}\nthe list of valid argument patterns is given by:\n${patterns}`;
}
const resolved = this.keys;
if (resolved) {
return resolved;
}
const members: string[] = [];
const keys = this.commandMap.keys();
let next: IteratorResult<string>;
// eslint-disable-next-line no-cond-assign
while (!(next = keys.next()).done) {
members.push(next.value);
}
return `${this.resolvedIdentifier()} commands: { ${members.sort().join(', ')} }`;
};
private success = (command: string) => `${this.resolvedIdentifier()} completed local execution of ${white(command)}`;
public registerCommand = (basename: string, argPatterns: (RegExp | string)[], action: ReplAction) => {
const existing = this.commandMap.get(basename);
const converted = argPatterns.map(input => (input instanceof RegExp ? input : new RegExp(input)));
const registration = { argPatterns: converted, action };
if (existing) {
existing.push(registration);
} else {
this.commandMap.set(basename, [registration]);
}
};
private invalid = (command: string, validCommand: boolean) => {
console.log(red(typeof this.onInvalid === 'string' ? this.onInvalid : this.onInvalid(command, validCommand)));
this.busy = false;
};
private valid = (command: string) => {
console.log(green(typeof this.onValid === 'string' ? this.onValid : this.onValid(command)));
this.busy = false;
};
private considerInput = async (lineIn: string) => {
if (this.busy) {
console.log(red('Busy'));
return;
}
this.busy = true;
let line = lineIn.trim();
if (this.isCaseSensitive) {
line = line.toLowerCase();
}
const [command, ...args] = line.split(/\s+/g);
if (!command) {
this.invalid(command, false);
return;
}
const registered = this.commandMap.get(command);
if (registered) {
const { length } = args;
const candidates = registered.filter(({ argPatterns: { length: count } }) => count === length);
candidates.forEach(({ argPatterns, action }: { argPatterns: any; action: any }) => {
const parsed: string[] = [];
let matched = true;
if (length) {
for (let i = 0; i < length; i++) {
const matches = argPatterns[i].exec(args[i]);
if (matches === null) {
matched = false;
break;
}
parsed.push(matches[0]);
}
}
if (!length || matched) {
const result = action(parsed);
const resolve = () => this.valid(`${command} ${parsed.join(' ')}`);
if (result instanceof Promise) {
result.then(resolve);
} else {
resolve();
}
}
});
this.invalid(command, true);
} else {
this.invalid(command, false);
}
};
}
|