blob: 701c19f6b14b8380ea2986c999ac6e49f615b77c [file] [log] [blame]
Robert Gindaf82267d2015-06-09 15:32:04 -07001// Copyright (c) 2015 The Chromium OS Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5'use strict';
6
7lib.rtdep('hterm.Keyboard.KeyActions');
8
9/**
10 * @constructor
11 * Parses the key definition syntax used for user keyboard customizations.
12 */
13hterm.Parser = function() {
14 /**
15 * @type {string} The source string.
16 */
17 this.source = '';
18
19 /**
20 * @type {number} The current position.
21 */
22 this.pos = 0;
23
24 /**
25 * @type {string?} The character at the current position.
26 */
27 this.ch = null;
28};
29
30hterm.Parser.prototype.error = function(message) {
31 return new Error('Parse error at ' + this.pos + ': ' + message);
32};
33
34hterm.Parser.prototype.isComplete = function() {
35 return this.pos == this.source.length;
36};
37
38hterm.Parser.prototype.reset = function(source, opt_pos) {
39 this.source = source;
40 this.pos = opt_pos || 0;
41 this.ch = source.substr(0, 1);
42};
43
44/**
45 * Parse a key sequence.
46 *
47 * Key sequences look like:
48 * "X", Just the letter X.
49 * 88, Another way of specifying the letter X. 88 is the keyCode for the 'xX'
50 * keycap.
51 * Ctrl-"X", Ctrl followed by the letter X.
52 * Ctrl-"x", Ctrl followed by the letter X.
53 * Ctrl-"8", Ctrl followed by the number 8 key.
54 * Ctrl-88, Another way of specifying Ctrl X.
55 * Ctrl-Alt-"[", Ctrl Alt and the open-square-bracket.
56 * Ctrl-Alt-[TAB], Ctrl Alt and the Tab key specified as a mnemonic.
57 *
58 * Modifier names are Ctrl, Alt, and Meta. Mnemonic names come from
59 * hterm_keyboard_keymap.js.
60 *
61 * @return {Object} An object with shift, ctrl, alt, meta, charCode
62 * properties.
63 */
64hterm.Parser.prototype.parseKeySequence = function() {
65 var rv = {
66 shift: false,
67 ctrl: false,
68 alt: false,
69 meta: false,
70 keyCode: null
71 };
72
73 while (this.pos < this.source.length) {
74 this.skipSpace();
75
76 var token = this.parseToken();
77 if (token.type == 'integer') {
78 rv.keyCode = token.value;
79
80 } else if (token.type == 'identifier') {
81 if (token.value.match(/^(shift|ctrl|alt|meta)$/i)) {
82 var mod = token.value.toLowerCase();
83 if (rv[mod] && rv[mod] != '*')
84 throw this.error('Duplicate modifier: ' + token.value);
85 rv[mod] = true;
86
87 } else if (token.value in hterm.Parser.identifiers.keys) {
88 rv.keyCode = hterm.Parser.identifiers.keys[token.value];
89
90 } else {
91 throw this.error('Unknown key: ' + token.value);
92 }
93
94 } else if (token.type == 'symbol') {
95 if (token.value == '*') {
96 ['shift', 'ctrl', 'alt', 'meta'].forEach(function(p) {
97 if (!rv[p])
98 rv[p] = '*';
99 });
100 } else {
101 throw this.error('Unexpected symbol: ' + token.value);
102 }
103 } else {
104 throw this.error('Expected integer or identifier');
105 }
106
107 this.skipSpace();
108
109 if (this.ch != '-')
110 break;
111
112 if (rv.keyCode != null)
113 throw this.error('Extra definition after target key');
114
115 this.advance(1);
116 }
117
118 if (rv.keyCode == null)
119 throw this.error('Missing target key');
120
121 return rv;
122};
123
124hterm.Parser.prototype.parseKeyAction = function() {
125 this.skipSpace();
126
127 var token = this.parseToken();
128
129 if (token.type == 'string')
130 return token.value;
131
132 if (token.type == 'identifier') {
133 if (token.value in hterm.Parser.identifiers.actions)
134 return hterm.Parser.identifiers.actions[token.value];
135
136 throw this.error('Unknown key action: ' + token.value);
137 }
138
139 throw this.error('Expected string or identifier');
140
141};
142
143hterm.Parser.prototype.peekString = function() {
144 return this.ch == '\'' || this.ch == '"';
145};
146
147hterm.Parser.prototype.peekIdentifier = function() {
148 return this.ch.match(/[a-z_]/i);
149};
150
151hterm.Parser.prototype.peekInteger = function() {
152 return this.ch.match(/[0-9]/);
153};
154
155hterm.Parser.prototype.parseToken = function() {
156 if (this.ch == '*') {
157 var rv = {type: 'symbol', value: this.ch};
158 this.advance(1);
159 return rv;
160 }
161
162 if (this.peekIdentifier())
163 return {type: 'identifier', value: this.parseIdentifier()};
164
165 if (this.peekString())
166 return {type: 'string', value: this.parseString()};
167
168 if (this.peekInteger())
169 return {type: 'integer', value: this.parseInteger()};
170
171
172 throw this.error('Unexpected token');
173};
174
175hterm.Parser.prototype.parseIdentifier = function() {
176 if (!this.peekIdentifier())
177 throw this.error('Expected identifier');
178
179 return this.parsePattern(/[a-z0-9_]+/ig);
180};
181
182hterm.Parser.prototype.parseInteger = function() {
183 var base = 10;
184
185 if (this.ch == '0' && this.pos < this.source.length - 1 &&
186 this.source.substr(this.pos + 1, 1) == 'x') {
187 return parseInt(this.parsePattern(/0x[0-9a-f]+/gi));
188 }
189
190 return parseInt(this.parsePattern(/\d+/g));
191};
192
193/**
194 * Parse a single or double quoted string.
195 *
196 * The current position should point at the initial quote character. Single
197 * quoted strings will be treated literally, double quoted will process escapes.
198 *
199 * TODO(rginda): Variable interpolation.
200 *
201 * @param {ParseState} parseState
202 * @param {string} quote A single or double-quote character.
203 * @return {string}
204 */
205hterm.Parser.prototype.parseString = function() {
206 var result = '';
207
208 var quote = this.ch;
209 if (quote != '"' && quote != '\'')
210 throw this.error('String expected');
211
212 this.advance(1);
213
214 var re = new RegExp('[\\\\' + quote + ']', 'g');
215
216 while (this.pos < this.source.length) {
217 re.lastIndex = this.pos;
218 if (!re.exec(this.source))
219 throw this.error('Unterminated string literal');
220
221 result += this.source.substring(this.pos, re.lastIndex - 1);
222
223 this.advance(re.lastIndex - this.pos - 1);
224
225 if (quote == '"' && this.ch == '\\') {
226 this.advance(1);
227 result += this.parseEscape();
228 continue;
229 }
230
231 if (quote == '\'' && this.ch == '\\') {
232 result += this.ch;
233 this.advance(1);
234 continue;
235 }
236
237 if (this.ch == quote) {
238 this.advance(1);
239 return result;
240 }
241 }
242
243 throw this.error('Unterminated string literal');
244};
245
246
247/**
248 * Parse an escape code from the current position (which should point to
249 * the first character AFTER the leading backslash.)
250 *
251 * @return {string}
252 */
253hterm.Parser.prototype.parseEscape = function() {
254 var map = {
255 '"': '"',
256 '\'': '\'',
257 '\\': '\\',
258 'a': '\x07',
259 'b': '\x08',
260 'e': '\x1b',
261 'f': '\x0c',
262 'n': '\x0a',
263 'r': '\x0d',
264 't': '\x09',
265 'v': '\x0b',
266 'x': function() {
267 var value = this.parsePattern(/[a-z0-9]{2}/ig);
268 return String.fromCharCode(parseInt(value, 16));
269 },
270 'u': function() {
271 var value = this.parsePattern(/[a-z0-9]{4}/ig);
272 return String.fromCharCode(parseInt(value, 16));
273 }
274 };
275
276 if (!(this.ch in map))
277 throw this.error('Unknown escape: ' + this.ch);
278
279 var value = map[this.ch];
280 this.advance(1);
281
282 if (typeof value == 'function')
283 value = value.call(this);
284
285 return value;
286};
287
288/**
289 * Parse the given pattern starting from the current position.
290 *
291 * @param {RegExp} pattern A pattern representing the characters to span. MUST
292 * include the "global" RegExp flag.
293 * @return {string}
294 */
295hterm.Parser.prototype.parsePattern = function(pattern) {
296 if (!pattern.global)
297 throw this.error('Internal error: Span patterns must be global');
298
299 pattern.lastIndex = this.pos;
300 var ary = pattern.exec(this.source);
301
302 if (!ary || pattern.lastIndex - ary[0].length != this.pos)
303 throw this.error('Expected match for: ' + pattern);
304
305 this.pos = pattern.lastIndex - 1;
306 this.advance(1);
307
308 return ary[0];
309};
310
311
312/**
313 * Advance the current position.
314 *
315 * @param {number} count
316 */
317hterm.Parser.prototype.advance = function(count) {
318 this.pos += count;
319 this.ch = this.source.substr(this.pos, 1);
320};
321
322/**
323 * @param {string=} opt_expect A list of valid non-whitespace characters to
324 * terminate on.
325 * @return {void}
326 */
327hterm.Parser.prototype.skipSpace = function(opt_expect) {
328 if (!/\s/.test(this.ch))
329 return;
330
331 var re = /\s+/gm;
332 re.lastIndex = this.pos;
333
334 var source = this.source;
335 if (re.exec(source))
336 this.pos = re.lastIndex;
337
338 this.ch = this.source.substr(this.pos, 1);
339
340 if (opt_expect) {
341 if (this.ch.indexOf(opt_expect) == -1) {
342 throw this.error('Expected one of ' + opt_expect + ', found: ' +
343 this.ch);
344 }
345 }
346};