From 5c100075f09493be7cdf3e0fb15416a9e676c8d5 Mon Sep 17 00:00:00 2001 From: cjihrig Date: Sun, 23 Jun 2019 11:54:43 -0400 Subject: lib: rename lib/internal/readline.js MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit moves lib/internal/readline.js to lib/internal/readline/utils.js. This is in preparation of adding a readline.promises implementation. PR-URL: https://github.com/nodejs/node/pull/28753 Reviewed-By: Michaƫl Zasso Reviewed-By: Luigi Pinca Reviewed-By: Rich Trott Reviewed-By: Anto Aravinth Reviewed-By: Trivikram Kamat Reviewed-By: Shingo Inoue --- lib/internal/readline/utils.js | 430 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 430 insertions(+) create mode 100644 lib/internal/readline/utils.js (limited to 'lib/internal/readline/utils.js') diff --git a/lib/internal/readline/utils.js b/lib/internal/readline/utils.js new file mode 100644 index 0000000000..c6cd13a6bd --- /dev/null +++ b/lib/internal/readline/utils.js @@ -0,0 +1,430 @@ +'use strict'; + +// Regex used for ansi escape code splitting +// Adopted from https://github.com/chalk/ansi-regex/blob/master/index.js +// License: MIT, authors: @sindresorhus, Qix-, and arjunmehta +// Matches all ansi escape code sequences in a string +/* eslint-disable no-control-regex */ +const ansi = + /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g; +/* eslint-enable no-control-regex */ +const kUTF16SurrogateThreshold = 0x10000; // 2 ** 16 +const kEscape = '\x1b'; + +let getStringWidth; +let isFullWidthCodePoint; + +function CSI(strings, ...args) { + let ret = `${kEscape}[`; + for (var n = 0; n < strings.length; n++) { + ret += strings[n]; + if (n < args.length) + ret += args[n]; + } + return ret; +} + +CSI.kEscape = kEscape; +CSI.kClearToBeginning = CSI`1K`; +CSI.kClearToEnd = CSI`0K`; +CSI.kClearLine = CSI`2K`; +CSI.kClearScreenDown = CSI`0J`; + +if (internalBinding('config').hasIntl) { + const icu = internalBinding('icu'); + getStringWidth = function getStringWidth(str, options) { + options = options || {}; + if (!Number.isInteger(str)) + str = stripVTControlCharacters(String(str)); + return icu.getStringWidth( + str, + Boolean(options.ambiguousAsFullWidth), + Boolean(options.expandEmojiSequence) + ); + }; + isFullWidthCodePoint = + function isFullWidthCodePoint(code, options) { + if (typeof code !== 'number') + return false; + return icu.getStringWidth(code, options) === 2; + }; +} else { + /** + * Returns the number of columns required to display the given string. + */ + getStringWidth = function getStringWidth(str) { + if (Number.isInteger(str)) + return isFullWidthCodePoint(str) ? 2 : 1; + + let width = 0; + + str = stripVTControlCharacters(String(str)); + + for (var i = 0; i < str.length; i++) { + const code = str.codePointAt(i); + + if (code >= kUTF16SurrogateThreshold) { // Surrogates. + i++; + } + + if (isFullWidthCodePoint(code)) { + width += 2; + } else { + width++; + } + } + + return width; + }; + + /** + * Returns true if the character represented by a given + * Unicode code point is full-width. Otherwise returns false. + */ + isFullWidthCodePoint = function isFullWidthCodePoint(code) { + // Code points are derived from: + // http://www.unicode.org/Public/UNIDATA/EastAsianWidth.txt + return Number.isInteger(code) && code >= 0x1100 && ( + code <= 0x115f || // Hangul Jamo + code === 0x2329 || // LEFT-POINTING ANGLE BRACKET + code === 0x232a || // RIGHT-POINTING ANGLE BRACKET + // CJK Radicals Supplement .. Enclosed CJK Letters and Months + code >= 0x2e80 && code <= 0x3247 && code !== 0x303f || + // Enclosed CJK Letters and Months .. CJK Unified Ideographs Extension A + code >= 0x3250 && code <= 0x4dbf || + // CJK Unified Ideographs .. Yi Radicals + code >= 0x4e00 && code <= 0xa4c6 || + // Hangul Jamo Extended-A + code >= 0xa960 && code <= 0xa97c || + // Hangul Syllables + code >= 0xac00 && code <= 0xd7a3 || + // CJK Compatibility Ideographs + code >= 0xf900 && code <= 0xfaff || + // Vertical Forms + code >= 0xfe10 && code <= 0xfe19 || + // CJK Compatibility Forms .. Small Form Variants + code >= 0xfe30 && code <= 0xfe6b || + // Halfwidth and Fullwidth Forms + code >= 0xff01 && code <= 0xff60 || + code >= 0xffe0 && code <= 0xffe6 || + // Kana Supplement + code >= 0x1b000 && code <= 0x1b001 || + // Enclosed Ideographic Supplement + code >= 0x1f200 && code <= 0x1f251 || + // CJK Unified Ideographs Extension B .. Tertiary Ideographic Plane + code >= 0x20000 && code <= 0x3fffd + ); + }; +} + +/** + * Tries to remove all VT control characters. Use to estimate displayed + * string width. May be buggy due to not running a real state machine + */ +function stripVTControlCharacters(str) { + return str.replace(ansi, ''); +} + + +/* + 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 + * + * So the generic regexp is like /^\d\d?(;\d)?[~^$]$/ + * + * + * 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); + } + } + + // 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 = s.slice(cmdStart); + let match; + + if ((match = cmd.match(/^(\d\d?)(;(\d))?([~^$])$/))) { + code += match[1] + match[4]; + modifier = (match[3] || 1) - 1; + } else if ((match = cmd.match(/^((\d;)?(\d))?([A-Za-z])$/))) { + 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 O letter */ + 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; + + /* 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'; + } else if (ch === '\n') { + // Enter, should have been called linefeed + key.name = 'enter'; + } else if (ch === '\t') { + // tab + key.name = 'tab'; + } 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(ch.charCodeAt(0) + 'a'.charCodeAt(0) - 1); + key.ctrl = true; + } else if (/^[0-9A-Za-z]$/.test(ch)) { + // Letter, number, shift+letter + key.name = ch.toLowerCase(); + key.shift = /^[A-Z]$/.test(ch); + 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 (s.length === 1) { + /* Single unnamed character, e.g. "." */ + stream.emit('keypress', s, key); + } + /* Unrecognized or broken escape sequence, don't emit anything */ + } +} + +module.exports = { + emitKeys, + getStringWidth, + isFullWidthCodePoint, + kUTF16SurrogateThreshold, + stripVTControlCharacters, + CSI +}; -- cgit v1.2.3