ink
Version:
React for CLI
154 lines • 5.01 kB
JavaScript
const escape = '\u001B';
const isCsiParameterByte = (byte) => {
return byte >= 0x30 && byte <= 0x3f;
};
const isCsiIntermediateByte = (byte) => {
return byte >= 0x20 && byte <= 0x2f;
};
const isCsiFinalByte = (byte) => {
return byte >= 0x40 && byte <= 0x7e;
};
const parseCsiSequence = (input, startIndex, prefixLength) => {
const csiPayloadStart = startIndex + prefixLength + 1;
let index = csiPayloadStart;
for (; index < input.length; index++) {
const byte = input.codePointAt(index);
if (byte === undefined) {
return 'pending';
}
if (isCsiParameterByte(byte) || isCsiIntermediateByte(byte)) {
continue;
}
// Preserve legacy terminal function-key sequences like ESC[[A and ESC[[5~.
if (byte === 0x5b && index === csiPayloadStart) {
continue;
}
if (isCsiFinalByte(byte)) {
return {
sequence: input.slice(startIndex, index + 1),
nextIndex: index + 1,
};
}
return undefined;
}
return 'pending';
};
const parseSs3Sequence = (input, startIndex, prefixLength) => {
const nextIndex = startIndex + prefixLength + 2;
if (nextIndex > input.length) {
return 'pending';
}
const finalByte = input.codePointAt(nextIndex - 1);
if (finalByte === undefined || !isCsiFinalByte(finalByte)) {
return undefined;
}
return {
sequence: input.slice(startIndex, nextIndex),
nextIndex,
};
};
const parseControlSequence = (input, startIndex, prefixLength) => {
const sequenceType = input[startIndex + prefixLength];
if (sequenceType === undefined) {
return 'pending';
}
if (sequenceType === '[') {
return parseCsiSequence(input, startIndex, prefixLength);
}
if (sequenceType === 'O') {
return parseSs3Sequence(input, startIndex, prefixLength);
}
return undefined;
};
const parseEscapedCodePoint = (input, escapeIndex) => {
const nextCodePoint = input.codePointAt(escapeIndex + 1);
const nextCodePointLength = nextCodePoint !== undefined && nextCodePoint > 0xff_ff ? 2 : 1;
const nextIndex = escapeIndex + 1 + nextCodePointLength;
return {
sequence: input.slice(escapeIndex, nextIndex),
nextIndex,
};
};
const parseKeypresses = (input) => {
const events = [];
let index = 0;
const pendingFrom = (pendingStartIndex) => ({
events,
pending: input.slice(pendingStartIndex),
});
while (index < input.length) {
const escapeIndex = input.indexOf(escape, index);
if (escapeIndex === -1) {
events.push(input.slice(index));
return {
events,
pending: '',
};
}
if (escapeIndex > index) {
events.push(input.slice(index, escapeIndex));
}
if (escapeIndex === input.length - 1) {
return pendingFrom(escapeIndex);
}
const parsedSequence = parseControlSequence(input, escapeIndex, 1);
if (parsedSequence === 'pending') {
return pendingFrom(escapeIndex);
}
if (parsedSequence) {
events.push(parsedSequence.sequence);
index = parsedSequence.nextIndex;
continue;
}
const next = input[escapeIndex + 1];
if (next === escape) {
if (escapeIndex + 2 >= input.length) {
return pendingFrom(escapeIndex);
}
const doubleEscapeSequence = parseControlSequence(input, escapeIndex, 2);
if (doubleEscapeSequence === 'pending') {
return pendingFrom(escapeIndex);
}
if (doubleEscapeSequence) {
events.push(doubleEscapeSequence.sequence);
index = doubleEscapeSequence.nextIndex;
continue;
}
events.push(input.slice(escapeIndex, escapeIndex + 2));
index = escapeIndex + 2;
continue;
}
const escapedCodePoint = parseEscapedCodePoint(input, escapeIndex);
events.push(escapedCodePoint.sequence);
index = escapedCodePoint.nextIndex;
}
return {
events,
pending: '',
};
};
export const createInputParser = () => {
let pending = '';
return {
push(chunk) {
const parsedInput = parseKeypresses(pending + chunk);
pending = parsedInput.pending;
return parsedInput.events;
},
hasPendingEscape() {
return pending.startsWith(escape);
},
flushPendingEscape() {
if (!pending.startsWith(escape)) {
return undefined;
}
const pendingEscape = pending;
pending = '';
return pendingEscape;
},
reset() {
pending = '';
},
};
};
//# sourceMappingURL=input-parser.js.map