Skip to content

Commit ca2e8b2

Browse files
committed
readline: deprecate undocumented exports
This commit moves several of readline's undocumented functions into an internal module. Specifically, isFullWidthCodePoint, stripVTControlCharacters, getStringWidth, and emitKeys are moved to the internal module. The existing public exports of the first three functions are given a deprecation notice. Refs: #3847 Fixes: #3836 PR-URL: #3862 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Brian White <mscdex@mscdex.net>
1 parent e499ea8 commit ca2e8b2

File tree

3 files changed

+410
-391
lines changed

3 files changed

+410
-391
lines changed

Diff for: lib/internal/readline.js

+392
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,392 @@
1+
'use strict';
2+
3+
// Regexes used for ansi escape code splitting
4+
const metaKeyCodeReAnywhere = /(?:\x1b)([a-zA-Z0-9])/;
5+
const functionKeyCodeReAnywhere = new RegExp('(?:\x1b+)(O|N|\\[|\\[\\[)(?:' + [
6+
'(\\d+)(?:;(\\d+))?([~^$])',
7+
'(?:M([@ #!a`])(.)(.))', // mouse
8+
'(?:1;)?(\\d+)?([a-zA-Z])'
9+
].join('|') + ')');
10+
11+
12+
module.exports = {
13+
emitKeys,
14+
getStringWidth,
15+
isFullWidthCodePoint,
16+
stripVTControlCharacters
17+
};
18+
19+
20+
/**
21+
* Returns the number of columns required to display the given string.
22+
*/
23+
function getStringWidth(str) {
24+
let width = 0;
25+
26+
str = stripVTControlCharacters(str);
27+
28+
for (let i = 0; i < str.length; i++) {
29+
const code = str.codePointAt(i);
30+
31+
if (code >= 0x10000) { // surrogates
32+
i++;
33+
}
34+
35+
if (isFullWidthCodePoint(code)) {
36+
width += 2;
37+
} else {
38+
width++;
39+
}
40+
}
41+
42+
return width;
43+
}
44+
45+
46+
/**
47+
* Returns true if the character represented by a given
48+
* Unicode code point is full-width. Otherwise returns false.
49+
*/
50+
function isFullWidthCodePoint(code) {
51+
if (isNaN(code)) {
52+
return false;
53+
}
54+
55+
// Code points are derived from:
56+
// http://www.unicode.org/Public/UNIDATA/EastAsianWidth.txt
57+
if (code >= 0x1100 && (
58+
code <= 0x115f || // Hangul Jamo
59+
0x2329 === code || // LEFT-POINTING ANGLE BRACKET
60+
0x232a === code || // RIGHT-POINTING ANGLE BRACKET
61+
// CJK Radicals Supplement .. Enclosed CJK Letters and Months
62+
(0x2e80 <= code && code <= 0x3247 && code !== 0x303f) ||
63+
// Enclosed CJK Letters and Months .. CJK Unified Ideographs Extension A
64+
0x3250 <= code && code <= 0x4dbf ||
65+
// CJK Unified Ideographs .. Yi Radicals
66+
0x4e00 <= code && code <= 0xa4c6 ||
67+
// Hangul Jamo Extended-A
68+
0xa960 <= code && code <= 0xa97c ||
69+
// Hangul Syllables
70+
0xac00 <= code && code <= 0xd7a3 ||
71+
// CJK Compatibility Ideographs
72+
0xf900 <= code && code <= 0xfaff ||
73+
// Vertical Forms
74+
0xfe10 <= code && code <= 0xfe19 ||
75+
// CJK Compatibility Forms .. Small Form Variants
76+
0xfe30 <= code && code <= 0xfe6b ||
77+
// Halfwidth and Fullwidth Forms
78+
0xff01 <= code && code <= 0xff60 ||
79+
0xffe0 <= code && code <= 0xffe6 ||
80+
// Kana Supplement
81+
0x1b000 <= code && code <= 0x1b001 ||
82+
// Enclosed Ideographic Supplement
83+
0x1f200 <= code && code <= 0x1f251 ||
84+
// CJK Unified Ideographs Extension B .. Tertiary Ideographic Plane
85+
0x20000 <= code && code <= 0x3fffd)) {
86+
return true;
87+
}
88+
89+
return false;
90+
}
91+
92+
93+
/**
94+
* Tries to remove all VT control characters. Use to estimate displayed
95+
* string width. May be buggy due to not running a real state machine
96+
*/
97+
function stripVTControlCharacters(str) {
98+
str = str.replace(new RegExp(functionKeyCodeReAnywhere.source, 'g'), '');
99+
return str.replace(new RegExp(metaKeyCodeReAnywhere.source, 'g'), '');
100+
}
101+
102+
103+
/*
104+
Some patterns seen in terminal key escape codes, derived from combos seen
105+
at http://www.midnight-commander.org/browser/lib/tty/key.c
106+
107+
ESC letter
108+
ESC [ letter
109+
ESC [ modifier letter
110+
ESC [ 1 ; modifier letter
111+
ESC [ num char
112+
ESC [ num ; modifier char
113+
ESC O letter
114+
ESC O modifier letter
115+
ESC O 1 ; modifier letter
116+
ESC N letter
117+
ESC [ [ num ; modifier char
118+
ESC [ [ 1 ; modifier letter
119+
ESC ESC [ num char
120+
ESC ESC O letter
121+
122+
- char is usually ~ but $ and ^ also happen with rxvt
123+
- modifier is 1 +
124+
(shift * 1) +
125+
(left_alt * 2) +
126+
(ctrl * 4) +
127+
(right_alt * 8)
128+
- two leading ESCs apparently mean the same as one leading ESC
129+
*/
130+
function* emitKeys(stream) {
131+
while (true) {
132+
let ch = yield;
133+
let s = ch;
134+
let escaped = false;
135+
const key = {
136+
sequence: null,
137+
name: undefined,
138+
ctrl: false,
139+
meta: false,
140+
shift: false
141+
};
142+
143+
if (ch === '\x1b') {
144+
escaped = true;
145+
s += (ch = yield);
146+
147+
if (ch === '\x1b') {
148+
s += (ch = yield);
149+
}
150+
}
151+
152+
if (escaped && (ch === 'O' || ch === '[')) {
153+
// ansi escape sequence
154+
let code = ch;
155+
let modifier = 0;
156+
157+
if (ch === 'O') {
158+
// ESC O letter
159+
// ESC O modifier letter
160+
s += (ch = yield);
161+
162+
if (ch >= '0' && ch <= '9') {
163+
modifier = (ch >> 0) - 1;
164+
s += (ch = yield);
165+
}
166+
167+
code += ch;
168+
} else if (ch === '[') {
169+
// ESC [ letter
170+
// ESC [ modifier letter
171+
// ESC [ [ modifier letter
172+
// ESC [ [ num char
173+
s += (ch = yield);
174+
175+
if (ch === '[') {
176+
// \x1b[[A
177+
// ^--- escape codes might have a second bracket
178+
code += ch;
179+
s += (ch = yield);
180+
}
181+
182+
/*
183+
* Here and later we try to buffer just enough data to get
184+
* a complete ascii sequence.
185+
*
186+
* We have basically two classes of ascii characters to process:
187+
*
188+
*
189+
* 1. `\x1b[24;5~` should be parsed as { code: '[24~', modifier: 5 }
190+
*
191+
* This particular example is featuring Ctrl+F12 in xterm.
192+
*
193+
* - `;5` part is optional, e.g. it could be `\x1b[24~`
194+
* - first part can contain one or two digits
195+
*
196+
* So the generic regexp is like /^\d\d?(;\d)?[~^$]$/
197+
*
198+
*
199+
* 2. `\x1b[1;5H` should be parsed as { code: '[H', modifier: 5 }
200+
*
201+
* This particular example is featuring Ctrl+Home in xterm.
202+
*
203+
* - `1;5` part is optional, e.g. it could be `\x1b[H`
204+
* - `1;` part is optional, e.g. it could be `\x1b[5H`
205+
*
206+
* So the generic regexp is like /^((\d;)?\d)?[A-Za-z]$/
207+
*
208+
*/
209+
const cmdStart = s.length - 1;
210+
211+
// skip one or two leading digits
212+
if (ch >= '0' && ch <= '9') {
213+
s += (ch = yield);
214+
215+
if (ch >= '0' && ch <= '9') {
216+
s += (ch = yield);
217+
}
218+
}
219+
220+
// skip modifier
221+
if (ch === ';') {
222+
s += (ch = yield);
223+
224+
if (ch >= '0' && ch <= '9') {
225+
s += (ch = yield);
226+
}
227+
}
228+
229+
/*
230+
* We buffered enough data, now trying to extract code
231+
* and modifier from it
232+
*/
233+
const cmd = s.slice(cmdStart);
234+
let match;
235+
236+
if ((match = cmd.match(/^(\d\d?)(;(\d))?([~^$])$/))) {
237+
code += match[1] + match[4];
238+
modifier = (match[3] || 1) - 1;
239+
} else if ((match = cmd.match(/^((\d;)?(\d))?([A-Za-z])$/))) {
240+
code += match[4];
241+
modifier = (match[3] || 1) - 1;
242+
} else {
243+
code += cmd;
244+
}
245+
}
246+
247+
// Parse the key modifier
248+
key.ctrl = !!(modifier & 4);
249+
key.meta = !!(modifier & 10);
250+
key.shift = !!(modifier & 1);
251+
key.code = code;
252+
253+
// Parse the key itself
254+
switch (code) {
255+
/* xterm/gnome ESC O letter */
256+
case 'OP': key.name = 'f1'; break;
257+
case 'OQ': key.name = 'f2'; break;
258+
case 'OR': key.name = 'f3'; break;
259+
case 'OS': key.name = 'f4'; break;
260+
261+
/* xterm/rxvt ESC [ number ~ */
262+
case '[11~': key.name = 'f1'; break;
263+
case '[12~': key.name = 'f2'; break;
264+
case '[13~': key.name = 'f3'; break;
265+
case '[14~': key.name = 'f4'; break;
266+
267+
/* from Cygwin and used in libuv */
268+
case '[[A': key.name = 'f1'; break;
269+
case '[[B': key.name = 'f2'; break;
270+
case '[[C': key.name = 'f3'; break;
271+
case '[[D': key.name = 'f4'; break;
272+
case '[[E': key.name = 'f5'; break;
273+
274+
/* common */
275+
case '[15~': key.name = 'f5'; break;
276+
case '[17~': key.name = 'f6'; break;
277+
case '[18~': key.name = 'f7'; break;
278+
case '[19~': key.name = 'f8'; break;
279+
case '[20~': key.name = 'f9'; break;
280+
case '[21~': key.name = 'f10'; break;
281+
case '[23~': key.name = 'f11'; break;
282+
case '[24~': key.name = 'f12'; break;
283+
284+
/* xterm ESC [ letter */
285+
case '[A': key.name = 'up'; break;
286+
case '[B': key.name = 'down'; break;
287+
case '[C': key.name = 'right'; break;
288+
case '[D': key.name = 'left'; break;
289+
case '[E': key.name = 'clear'; break;
290+
case '[F': key.name = 'end'; break;
291+
case '[H': key.name = 'home'; break;
292+
293+
/* xterm/gnome ESC O letter */
294+
case 'OA': key.name = 'up'; break;
295+
case 'OB': key.name = 'down'; break;
296+
case 'OC': key.name = 'right'; break;
297+
case 'OD': key.name = 'left'; break;
298+
case 'OE': key.name = 'clear'; break;
299+
case 'OF': key.name = 'end'; break;
300+
case 'OH': key.name = 'home'; break;
301+
302+
/* xterm/rxvt ESC [ number ~ */
303+
case '[1~': key.name = 'home'; break;
304+
case '[2~': key.name = 'insert'; break;
305+
case '[3~': key.name = 'delete'; break;
306+
case '[4~': key.name = 'end'; break;
307+
case '[5~': key.name = 'pageup'; break;
308+
case '[6~': key.name = 'pagedown'; break;
309+
310+
/* putty */
311+
case '[[5~': key.name = 'pageup'; break;
312+
case '[[6~': key.name = 'pagedown'; break;
313+
314+
/* rxvt */
315+
case '[7~': key.name = 'home'; break;
316+
case '[8~': key.name = 'end'; break;
317+
318+
/* rxvt keys with modifiers */
319+
case '[a': key.name = 'up'; key.shift = true; break;
320+
case '[b': key.name = 'down'; key.shift = true; break;
321+
case '[c': key.name = 'right'; key.shift = true; break;
322+
case '[d': key.name = 'left'; key.shift = true; break;
323+
case '[e': key.name = 'clear'; key.shift = true; break;
324+
325+
case '[2$': key.name = 'insert'; key.shift = true; break;
326+
case '[3$': key.name = 'delete'; key.shift = true; break;
327+
case '[5$': key.name = 'pageup'; key.shift = true; break;
328+
case '[6$': key.name = 'pagedown'; key.shift = true; break;
329+
case '[7$': key.name = 'home'; key.shift = true; break;
330+
case '[8$': key.name = 'end'; key.shift = true; break;
331+
332+
case 'Oa': key.name = 'up'; key.ctrl = true; break;
333+
case 'Ob': key.name = 'down'; key.ctrl = true; break;
334+
case 'Oc': key.name = 'right'; key.ctrl = true; break;
335+
case 'Od': key.name = 'left'; key.ctrl = true; break;
336+
case 'Oe': key.name = 'clear'; key.ctrl = true; break;
337+
338+
case '[2^': key.name = 'insert'; key.ctrl = true; break;
339+
case '[3^': key.name = 'delete'; key.ctrl = true; break;
340+
case '[5^': key.name = 'pageup'; key.ctrl = true; break;
341+
case '[6^': key.name = 'pagedown'; key.ctrl = true; break;
342+
case '[7^': key.name = 'home'; key.ctrl = true; break;
343+
case '[8^': key.name = 'end'; key.ctrl = true; break;
344+
345+
/* misc. */
346+
case '[Z': key.name = 'tab'; key.shift = true; break;
347+
default: key.name = 'undefined'; break;
348+
}
349+
} else if (ch === '\r') {
350+
// carriage return
351+
key.name = 'return';
352+
} else if (ch === '\n') {
353+
// enter, should have been called linefeed
354+
key.name = 'enter';
355+
} else if (ch === '\t') {
356+
// tab
357+
key.name = 'tab';
358+
} else if (ch === '\b' || ch === '\x7f') {
359+
// backspace or ctrl+h
360+
key.name = 'backspace';
361+
key.meta = escaped;
362+
} else if (ch === '\x1b') {
363+
// escape key
364+
key.name = 'escape';
365+
key.meta = escaped;
366+
} else if (ch === ' ') {
367+
key.name = 'space';
368+
key.meta = escaped;
369+
} else if (!escaped && ch <= '\x1a') {
370+
// ctrl+letter
371+
key.name = String.fromCharCode(ch.charCodeAt(0) + 'a'.charCodeAt(0) - 1);
372+
key.ctrl = true;
373+
} else if (/^[0-9A-Za-z]$/.test(ch)) {
374+
// letter, number, shift+letter
375+
key.name = ch.toLowerCase();
376+
key.shift = /^[A-Z]$/.test(ch);
377+
key.meta = escaped;
378+
}
379+
380+
key.sequence = s;
381+
382+
if (key.name !== undefined) {
383+
/* Named character or sequence */
384+
stream.emit('keypress', escaped ? undefined : s, key);
385+
} else if (s.length === 1) {
386+
/* Single unnamed character, e.g. "." */
387+
stream.emit('keypress', s);
388+
} else {
389+
/* Unrecognized or broken escape sequence, don't emit anything */
390+
}
391+
}
392+
}

0 commit comments

Comments
 (0)