nstdlib-nightly
Version:
Node.js standard library converted to runtime-agnostic ES modules.
576 lines (531 loc) • 14.1 kB
JavaScript
// Source: https://github.com/nodejs/node/blob/65eff1eb/lib/internal/readline/utils.js
const kUTF16SurrogateThreshold = 0x10000; // 2 ** 16
const kEscape = "\x1b";
const kSubstringSearch = Symbol("kSubstringSearch");
function CSI(strings, ...args) {
let ret = `${kEscape}[`;
for (let n = 0; n < strings.length; n++) {
ret += strings[n];
if (n < args.length) ret += args[n];
}
return ret;
}
CSI.kEscape = kEscape;
CSI.kClearToLineBeginning = CSI`1K`;
CSI.kClearToLineEnd = CSI`0K`;
CSI.kClearLine = CSI`2K`;
CSI.kClearScreenDown = CSI`0J`;
// TODO(BridgeAR): Treat combined characters as single character, i.e,
// 'a\u0301' and '\u0301a' (both have the same visual output).
// Check Canonical_Combining_Class in
// http://userguide.icu-project.org/strings/properties
function charLengthLeft(str, i) {
if (i <= 0) return 0;
if (
(i > 1 &&
String.prototype.codePointAt.call(str, i - 2) >=
kUTF16SurrogateThreshold) ||
String.prototype.codePointAt.call(str, i - 1) >= kUTF16SurrogateThreshold
) {
return 2;
}
return 1;
}
function charLengthAt(str, i) {
if (str.length <= i) {
// Pretend to move to the right. This is necessary to autocomplete while
// moving to the right.
return 1;
}
return String.prototype.codePointAt.call(str, i) >= kUTF16SurrogateThreshold
? 2
: 1;
}
/*
Some patterns seen in terminal key escape codes, derived from combos seen
at http://www.midnight-commander.org/browser/lib/tty/key.c
ESC letter
ESC [ letter
ESC [ modifier letter
ESC [ 1 ; modifier letter
ESC [ num char
ESC [ num ; modifier char
ESC O letter
ESC O modifier letter
ESC O 1 ; modifier letter
ESC N letter
ESC [ [ num ; modifier char
ESC [ [ 1 ; modifier letter
ESC ESC [ num char
ESC ESC O letter
- char is usually ~ but $ and ^ also happen with rxvt
- modifier is 1 +
(shift * 1) +
(left_alt * 2) +
(ctrl * 4) +
(right_alt * 8)
- two leading ESCs apparently mean the same as one leading ESC
*/
function* emitKeys(stream) {
while (true) {
let ch = yield;
let s = ch;
let escaped = false;
const key = {
sequence: null,
name: undefined,
ctrl: false,
meta: false,
shift: false,
};
if (ch === kEscape) {
escaped = true;
s += ch = yield;
if (ch === kEscape) {
s += ch = yield;
}
}
if (escaped && (ch === "O" || ch === "[")) {
// ANSI escape sequence
let code = ch;
let modifier = 0;
if (ch === "O") {
// ESC O letter
// ESC O modifier letter
s += ch = yield;
if (ch >= "0" && ch <= "9") {
modifier = (ch >> 0) - 1;
s += ch = yield;
}
code += ch;
} else if (ch === "[") {
// ESC [ letter
// ESC [ modifier letter
// ESC [ [ modifier letter
// ESC [ [ num char
s += ch = yield;
if (ch === "[") {
// \x1b[[A
// ^--- escape codes might have a second bracket
code += ch;
s += ch = yield;
}
/*
* Here and later we try to buffer just enough data to get
* a complete ascii sequence.
*
* We have basically two classes of ascii characters to process:
*
*
* 1. `\x1b[24;5~` should be parsed as { code: '[24~', modifier: 5 }
*
* This particular example is featuring Ctrl+F12 in xterm.
*
* - `;5` part is optional, e.g. it could be `\x1b[24~`
* - first part can contain one or two digits
* - there is also special case when there can be 3 digits
* but without modifier. They are the case of paste bracket mode
*
* So the generic regexp is like /^(?:\d\d?(;\d)?[~^$]|\d{3}~)$/
*
*
* 2. `\x1b[1;5H` should be parsed as { code: '[H', modifier: 5 }
*
* This particular example is featuring Ctrl+Home in xterm.
*
* - `1;5` part is optional, e.g. it could be `\x1b[H`
* - `1;` part is optional, e.g. it could be `\x1b[5H`
*
* So the generic regexp is like /^((\d;)?\d)?[A-Za-z]$/
*
*/
const cmdStart = s.length - 1;
// Skip one or two leading digits
if (ch >= "0" && ch <= "9") {
s += ch = yield;
if (ch >= "0" && ch <= "9") {
s += ch = yield;
if (ch >= "0" && ch <= "9") {
s += ch = yield;
}
}
}
// skip modifier
if (ch === ";") {
s += ch = yield;
if (ch >= "0" && ch <= "9") {
s += yield;
}
}
/*
* We buffered enough data, now trying to extract code
* and modifier from it
*/
const cmd = String.prototype.slice.call(s, cmdStart);
let match;
if (
(match = RegExp.prototype.exec.call(
/^(?:(\d\d?)(?:;(\d))?([~^$])|(\d{3}~))$/,
cmd,
))
) {
if (match[4]) {
code += match[4];
} else {
code += match[1] + match[3];
modifier = (match[2] || 1) - 1;
}
} else if (
(match = RegExp.prototype.exec.call(/^((\d;)?(\d))?([A-Za-z])$/, cmd))
) {
code += match[4];
modifier = (match[3] || 1) - 1;
} else {
code += cmd;
}
}
// Parse the key modifier
key.ctrl = !!(modifier & 4);
key.meta = !!(modifier & 10);
key.shift = !!(modifier & 1);
key.code = code;
// Parse the key itself
switch (code) {
/* xterm/gnome ESC [ letter (with modifier) */
case "[P":
key.name = "f1";
break;
case "[Q":
key.name = "f2";
break;
case "[R":
key.name = "f3";
break;
case "[S":
key.name = "f4";
break;
/* xterm/gnome ESC O letter (without modifier) */
case "OP":
key.name = "f1";
break;
case "OQ":
key.name = "f2";
break;
case "OR":
key.name = "f3";
break;
case "OS":
key.name = "f4";
break;
/* xterm/rxvt ESC [ number ~ */
case "[11~":
key.name = "f1";
break;
case "[12~":
key.name = "f2";
break;
case "[13~":
key.name = "f3";
break;
case "[14~":
key.name = "f4";
break;
/* paste bracket mode */
case "[200~":
key.name = "paste-start";
break;
case "[201~":
key.name = "paste-end";
break;
/* from Cygwin and used in libuv */
case "[[A":
key.name = "f1";
break;
case "[[B":
key.name = "f2";
break;
case "[[C":
key.name = "f3";
break;
case "[[D":
key.name = "f4";
break;
case "[[E":
key.name = "f5";
break;
/* common */
case "[15~":
key.name = "f5";
break;
case "[17~":
key.name = "f6";
break;
case "[18~":
key.name = "f7";
break;
case "[19~":
key.name = "f8";
break;
case "[20~":
key.name = "f9";
break;
case "[21~":
key.name = "f10";
break;
case "[23~":
key.name = "f11";
break;
case "[24~":
key.name = "f12";
break;
/* xterm ESC [ letter */
case "[A":
key.name = "up";
break;
case "[B":
key.name = "down";
break;
case "[C":
key.name = "right";
break;
case "[D":
key.name = "left";
break;
case "[E":
key.name = "clear";
break;
case "[F":
key.name = "end";
break;
case "[H":
key.name = "home";
break;
/* xterm/gnome ESC O letter */
case "OA":
key.name = "up";
break;
case "OB":
key.name = "down";
break;
case "OC":
key.name = "right";
break;
case "OD":
key.name = "left";
break;
case "OE":
key.name = "clear";
break;
case "OF":
key.name = "end";
break;
case "OH":
key.name = "home";
break;
/* xterm/rxvt ESC [ number ~ */
case "[1~":
key.name = "home";
break;
case "[2~":
key.name = "insert";
break;
case "[3~":
key.name = "delete";
break;
case "[4~":
key.name = "end";
break;
case "[5~":
key.name = "pageup";
break;
case "[6~":
key.name = "pagedown";
break;
/* putty */
case "[[5~":
key.name = "pageup";
break;
case "[[6~":
key.name = "pagedown";
break;
/* rxvt */
case "[7~":
key.name = "home";
break;
case "[8~":
key.name = "end";
break;
/* rxvt keys with modifiers */
case "[a":
key.name = "up";
key.shift = true;
break;
case "[b":
key.name = "down";
key.shift = true;
break;
case "[c":
key.name = "right";
key.shift = true;
break;
case "[d":
key.name = "left";
key.shift = true;
break;
case "[e":
key.name = "clear";
key.shift = true;
break;
case "[2$":
key.name = "insert";
key.shift = true;
break;
case "[3$":
key.name = "delete";
key.shift = true;
break;
case "[5$":
key.name = "pageup";
key.shift = true;
break;
case "[6$":
key.name = "pagedown";
key.shift = true;
break;
case "[7$":
key.name = "home";
key.shift = true;
break;
case "[8$":
key.name = "end";
key.shift = true;
break;
case "Oa":
key.name = "up";
key.ctrl = true;
break;
case "Ob":
key.name = "down";
key.ctrl = true;
break;
case "Oc":
key.name = "right";
key.ctrl = true;
break;
case "Od":
key.name = "left";
key.ctrl = true;
break;
case "Oe":
key.name = "clear";
key.ctrl = true;
break;
case "[2^":
key.name = "insert";
key.ctrl = true;
break;
case "[3^":
key.name = "delete";
key.ctrl = true;
break;
case "[5^":
key.name = "pageup";
key.ctrl = true;
break;
case "[6^":
key.name = "pagedown";
key.ctrl = true;
break;
case "[7^":
key.name = "home";
key.ctrl = true;
break;
case "[8^":
key.name = "end";
key.ctrl = true;
break;
/* misc. */
case "[Z":
key.name = "tab";
key.shift = true;
break;
default:
key.name = "undefined";
break;
}
} else if (ch === "\r") {
// carriage return
key.name = "return";
key.meta = escaped;
} else if (ch === "\n") {
// Enter, should have been called linefeed
key.name = "enter";
key.meta = escaped;
} else if (ch === "\t") {
// tab
key.name = "tab";
key.meta = escaped;
} else if (ch === "\b" || ch === "\x7f") {
// backspace or ctrl+h
key.name = "backspace";
key.meta = escaped;
} else if (ch === kEscape) {
// escape key
key.name = "escape";
key.meta = escaped;
} else if (ch === " ") {
key.name = "space";
key.meta = escaped;
} else if (!escaped && ch <= "\x1a") {
// ctrl+letter
key.name = String.fromCharCode(
String.prototype.charCodeAt.call(ch) +
String.prototype.charCodeAt.call("a") -
1,
);
key.ctrl = true;
} else if (RegExp.prototype.exec.call(/^[0-9A-Za-z]$/, ch) !== null) {
// Letter, number, shift+letter
key.name = String.prototype.toLowerCase.call(ch);
key.shift = RegExp.prototype.exec.call(/^[A-Z]$/, ch) !== null;
key.meta = escaped;
} else if (escaped) {
// Escape sequence timeout
key.name = ch.length ? undefined : "escape";
key.meta = true;
}
key.sequence = s;
if (s.length !== 0 && (key.name !== undefined || escaped)) {
/* Named character or sequence */
stream.emit("keypress", escaped ? undefined : s, key);
} else if (charLengthAt(s, 0) === s.length) {
/* Single unnamed character, e.g. "." */
stream.emit("keypress", s, key);
}
/* Unrecognized or broken escape sequence, don't emit anything */
}
}
// This runs in O(n log n).
function commonPrefix(strings) {
if (strings.length === 0) {
return "";
}
if (strings.length === 1) {
return strings[0];
}
const sorted = Array.prototype.toSorted.call(strings);
const min = sorted[0];
const max = sorted[sorted.length - 1];
for (let i = 0; i < min.length; i++) {
if (min[i] !== max[i]) {
return String.prototype.slice.call(min, 0, i);
}
}
return min;
}
export { charLengthAt };
export { charLengthLeft };
export { commonPrefix };
export { emitKeys };
export { kSubstringSearch };
export { CSI };