blob: 27bc32463451dbc3f62ce93531c8b1c7b76c76e7 [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
Robert Gindaf82267d2015-06-09 15:32:04 -07007/**
Robert Gindaf82267d2015-06-09 15:32:04 -07008 * Parses the key definition syntax used for user keyboard customizations.
Joel Hockey0f933582019-08-27 18:01:51 -07009 * @constructor
Robert Gindaf82267d2015-06-09 15:32:04 -070010 */
11hterm.Parser = function() {
12 /**
13 * @type {string} The source string.
14 */
15 this.source = '';
16
17 /**
18 * @type {number} The current position.
19 */
20 this.pos = 0;
21
22 /**
Joel Hockey0f933582019-08-27 18:01:51 -070023 * @type {?string} The character at the current position.
Robert Gindaf82267d2015-06-09 15:32:04 -070024 */
25 this.ch = null;
26};
27
Joel Hockey0f933582019-08-27 18:01:51 -070028/**
29 * @param {string} message
30 * @return {!Error}
31 */
Robert Gindaf82267d2015-06-09 15:32:04 -070032hterm.Parser.prototype.error = function(message) {
33 return new Error('Parse error at ' + this.pos + ': ' + message);
34};
35
Joel Hockey0f933582019-08-27 18:01:51 -070036/** @return {boolean} */
Robert Gindaf82267d2015-06-09 15:32:04 -070037hterm.Parser.prototype.isComplete = function() {
38 return this.pos == this.source.length;
39};
40
Joel Hockey0f933582019-08-27 18:01:51 -070041/**
42 * @param {string} source
43 * @param {number=} opt_pos
44 */
Robert Gindaf82267d2015-06-09 15:32:04 -070045hterm.Parser.prototype.reset = function(source, opt_pos) {
46 this.source = source;
47 this.pos = opt_pos || 0;
48 this.ch = source.substr(0, 1);
49};
50
51/**
52 * Parse a key sequence.
53 *
Robert Gindaca5b2872015-06-15 14:24:52 -070054 * A key sequence is zero or more of the key modifiers defined in
55 * hterm.Parser.identifiers.modifierKeys followed by a key code. Key
56 * codes can be an integer or an identifier from
57 * hterm.Parser.identifiers.keyCodes. Modifiers and keyCodes should be joined
58 * by the dash character.
Robert Gindaf82267d2015-06-09 15:32:04 -070059 *
Robert Gindaca5b2872015-06-15 14:24:52 -070060 * An asterisk "*" can be used to indicate that the unspecified modifiers
61 * are optional.
Robert Gindaf82267d2015-06-09 15:32:04 -070062 *
Robert Gindaca5b2872015-06-15 14:24:52 -070063 * For example:
64 * A: Matches only an unmodified "A" character.
65 * 65: Same as above.
66 * 0x41: Same as above.
67 * Ctrl-A: Matches only Ctrl-A.
68 * Ctrl-65: Same as above.
69 * Ctrl-0x41: Same as above.
70 * Ctrl-Shift-A: Matches only Ctrl-Shift-A.
71 * Ctrl-*-A: Matches Ctrl-A, as well as any other key sequence that includes
72 * at least the Ctrl and A keys.
73 *
Joel Hockey0f933582019-08-27 18:01:51 -070074 * @return {!hterm.Keyboard.KeyDown} An object with shift, ctrl, alt, meta,
75 * keyCode properties.
Robert Gindaf82267d2015-06-09 15:32:04 -070076 */
77hterm.Parser.prototype.parseKeySequence = function() {
78 var rv = {
Robert Gindaf82267d2015-06-09 15:32:04 -070079 keyCode: null
80 };
81
Robert Gindaca5b2872015-06-15 14:24:52 -070082 for (var k in hterm.Parser.identifiers.modifierKeys) {
83 rv[hterm.Parser.identifiers.modifierKeys[k]] = false;
84 }
85
Robert Gindaf82267d2015-06-09 15:32:04 -070086 while (this.pos < this.source.length) {
87 this.skipSpace();
88
89 var token = this.parseToken();
90 if (token.type == 'integer') {
91 rv.keyCode = token.value;
92
93 } else if (token.type == 'identifier') {
Mike Frysingere6721672017-05-26 01:26:55 -040094 var ucValue = token.value.toUpperCase();
95 if (ucValue in hterm.Parser.identifiers.modifierKeys &&
96 hterm.Parser.identifiers.modifierKeys.hasOwnProperty(ucValue)) {
97 var mod = hterm.Parser.identifiers.modifierKeys[ucValue];
Robert Gindaf82267d2015-06-09 15:32:04 -070098 if (rv[mod] && rv[mod] != '*')
99 throw this.error('Duplicate modifier: ' + token.value);
100 rv[mod] = true;
101
Mike Frysingere6721672017-05-26 01:26:55 -0400102 } else if (ucValue in hterm.Parser.identifiers.keyCodes &&
103 hterm.Parser.identifiers.keyCodes.hasOwnProperty(ucValue)) {
104 rv.keyCode = hterm.Parser.identifiers.keyCodes[ucValue];
Robert Gindaf82267d2015-06-09 15:32:04 -0700105
106 } else {
107 throw this.error('Unknown key: ' + token.value);
108 }
109
110 } else if (token.type == 'symbol') {
111 if (token.value == '*') {
Robert Gindaca5b2872015-06-15 14:24:52 -0700112 for (var id in hterm.Parser.identifiers.modifierKeys) {
113 var p = hterm.Parser.identifiers.modifierKeys[id];
Robert Gindaf82267d2015-06-09 15:32:04 -0700114 if (!rv[p])
115 rv[p] = '*';
Robert Gindaca5b2872015-06-15 14:24:52 -0700116 }
Robert Gindaf82267d2015-06-09 15:32:04 -0700117 } else {
118 throw this.error('Unexpected symbol: ' + token.value);
119 }
120 } else {
121 throw this.error('Expected integer or identifier');
122 }
123
124 this.skipSpace();
125
126 if (this.ch != '-')
127 break;
128
129 if (rv.keyCode != null)
130 throw this.error('Extra definition after target key');
131
132 this.advance(1);
133 }
134
135 if (rv.keyCode == null)
136 throw this.error('Missing target key');
137
138 return rv;
139};
140
Joel Hockey0f933582019-08-27 18:01:51 -0700141/** @return {string} */
Robert Gindaf82267d2015-06-09 15:32:04 -0700142hterm.Parser.prototype.parseKeyAction = function() {
143 this.skipSpace();
144
145 var token = this.parseToken();
146
147 if (token.type == 'string')
148 return token.value;
149
150 if (token.type == 'identifier') {
Mike Frysinger9837fff2017-05-26 00:40:41 -0400151 if (token.value in hterm.Parser.identifiers.actions &&
152 hterm.Parser.identifiers.actions.hasOwnProperty(token.value))
Robert Gindaf82267d2015-06-09 15:32:04 -0700153 return hterm.Parser.identifiers.actions[token.value];
154
155 throw this.error('Unknown key action: ' + token.value);
156 }
157
158 throw this.error('Expected string or identifier');
159
160};
161
Joel Hockey0f933582019-08-27 18:01:51 -0700162/** @return {boolean} */
Robert Gindaf82267d2015-06-09 15:32:04 -0700163hterm.Parser.prototype.peekString = function() {
164 return this.ch == '\'' || this.ch == '"';
165};
166
Joel Hockey0f933582019-08-27 18:01:51 -0700167/** @return {boolean} */
Robert Gindaf82267d2015-06-09 15:32:04 -0700168hterm.Parser.prototype.peekIdentifier = function() {
Joel Hockeyc98a0ce2019-09-20 16:26:20 -0700169 return !!this.ch.match(/[a-z_]/i);
Robert Gindaf82267d2015-06-09 15:32:04 -0700170};
171
Joel Hockey0f933582019-08-27 18:01:51 -0700172/** @return {boolean} */
Robert Gindaf82267d2015-06-09 15:32:04 -0700173hterm.Parser.prototype.peekInteger = function() {
Joel Hockeyc98a0ce2019-09-20 16:26:20 -0700174 return !!this.ch.match(/[0-9]/);
Robert Gindaf82267d2015-06-09 15:32:04 -0700175};
176
Joel Hockey0f933582019-08-27 18:01:51 -0700177/** @return {!Object} */
Robert Gindaf82267d2015-06-09 15:32:04 -0700178hterm.Parser.prototype.parseToken = function() {
179 if (this.ch == '*') {
180 var rv = {type: 'symbol', value: this.ch};
181 this.advance(1);
182 return rv;
183 }
184
185 if (this.peekIdentifier())
186 return {type: 'identifier', value: this.parseIdentifier()};
187
188 if (this.peekString())
189 return {type: 'string', value: this.parseString()};
190
191 if (this.peekInteger())
192 return {type: 'integer', value: this.parseInteger()};
193
194
195 throw this.error('Unexpected token');
196};
197
Joel Hockey0f933582019-08-27 18:01:51 -0700198/** @return {string} */
Robert Gindaf82267d2015-06-09 15:32:04 -0700199hterm.Parser.prototype.parseIdentifier = function() {
200 if (!this.peekIdentifier())
201 throw this.error('Expected identifier');
202
203 return this.parsePattern(/[a-z0-9_]+/ig);
204};
205
Joel Hockey0f933582019-08-27 18:01:51 -0700206/** @return {number} */
Robert Gindaf82267d2015-06-09 15:32:04 -0700207hterm.Parser.prototype.parseInteger = function() {
Robert Gindaf82267d2015-06-09 15:32:04 -0700208 if (this.ch == '0' && this.pos < this.source.length - 1 &&
209 this.source.substr(this.pos + 1, 1) == 'x') {
Joel Hockeyc98a0ce2019-09-20 16:26:20 -0700210 return parseInt(this.parsePattern(/0x[0-9a-f]+/gi), undefined);
Robert Gindaf82267d2015-06-09 15:32:04 -0700211 }
212
Joel Hockeyc98a0ce2019-09-20 16:26:20 -0700213 return parseInt(this.parsePattern(/\d+/g), 10);
Robert Gindaf82267d2015-06-09 15:32:04 -0700214};
215
216/**
217 * Parse a single or double quoted string.
218 *
219 * The current position should point at the initial quote character. Single
220 * quoted strings will be treated literally, double quoted will process escapes.
221 *
222 * TODO(rginda): Variable interpolation.
223 *
Robert Gindaf82267d2015-06-09 15:32:04 -0700224 * @return {string}
225 */
226hterm.Parser.prototype.parseString = function() {
227 var result = '';
228
229 var quote = this.ch;
230 if (quote != '"' && quote != '\'')
231 throw this.error('String expected');
232
233 this.advance(1);
234
235 var re = new RegExp('[\\\\' + quote + ']', 'g');
236
237 while (this.pos < this.source.length) {
238 re.lastIndex = this.pos;
239 if (!re.exec(this.source))
240 throw this.error('Unterminated string literal');
241
242 result += this.source.substring(this.pos, re.lastIndex - 1);
243
244 this.advance(re.lastIndex - this.pos - 1);
245
246 if (quote == '"' && this.ch == '\\') {
247 this.advance(1);
248 result += this.parseEscape();
249 continue;
250 }
251
252 if (quote == '\'' && this.ch == '\\') {
253 result += this.ch;
254 this.advance(1);
255 continue;
256 }
257
258 if (this.ch == quote) {
259 this.advance(1);
260 return result;
261 }
262 }
263
264 throw this.error('Unterminated string literal');
265};
266
267
268/**
269 * Parse an escape code from the current position (which should point to
270 * the first character AFTER the leading backslash.)
271 *
272 * @return {string}
273 */
274hterm.Parser.prototype.parseEscape = function() {
275 var map = {
276 '"': '"',
277 '\'': '\'',
278 '\\': '\\',
279 'a': '\x07',
280 'b': '\x08',
281 'e': '\x1b',
282 'f': '\x0c',
283 'n': '\x0a',
284 'r': '\x0d',
285 't': '\x09',
286 'v': '\x0b',
287 'x': function() {
288 var value = this.parsePattern(/[a-z0-9]{2}/ig);
289 return String.fromCharCode(parseInt(value, 16));
290 },
291 'u': function() {
292 var value = this.parsePattern(/[a-z0-9]{4}/ig);
293 return String.fromCharCode(parseInt(value, 16));
294 }
295 };
296
Mike Frysinger9837fff2017-05-26 00:40:41 -0400297 if (!(this.ch in map && map.hasOwnProperty(this.ch)))
Robert Gindaf82267d2015-06-09 15:32:04 -0700298 throw this.error('Unknown escape: ' + this.ch);
299
300 var value = map[this.ch];
301 this.advance(1);
302
303 if (typeof value == 'function')
304 value = value.call(this);
305
306 return value;
307};
308
309/**
310 * Parse the given pattern starting from the current position.
311 *
Joel Hockey0f933582019-08-27 18:01:51 -0700312 * @param {!RegExp} pattern A pattern representing the characters to span. MUST
Robert Gindaf82267d2015-06-09 15:32:04 -0700313 * include the "global" RegExp flag.
314 * @return {string}
315 */
316hterm.Parser.prototype.parsePattern = function(pattern) {
317 if (!pattern.global)
318 throw this.error('Internal error: Span patterns must be global');
319
320 pattern.lastIndex = this.pos;
321 var ary = pattern.exec(this.source);
322
323 if (!ary || pattern.lastIndex - ary[0].length != this.pos)
324 throw this.error('Expected match for: ' + pattern);
325
326 this.pos = pattern.lastIndex - 1;
327 this.advance(1);
328
329 return ary[0];
330};
331
332
333/**
334 * Advance the current position.
335 *
336 * @param {number} count
337 */
338hterm.Parser.prototype.advance = function(count) {
339 this.pos += count;
340 this.ch = this.source.substr(this.pos, 1);
341};
342
343/**
344 * @param {string=} opt_expect A list of valid non-whitespace characters to
345 * terminate on.
346 * @return {void}
347 */
348hterm.Parser.prototype.skipSpace = function(opt_expect) {
349 if (!/\s/.test(this.ch))
350 return;
351
352 var re = /\s+/gm;
353 re.lastIndex = this.pos;
354
355 var source = this.source;
356 if (re.exec(source))
357 this.pos = re.lastIndex;
358
359 this.ch = this.source.substr(this.pos, 1);
360
361 if (opt_expect) {
362 if (this.ch.indexOf(opt_expect) == -1) {
363 throw this.error('Expected one of ' + opt_expect + ', found: ' +
364 this.ch);
365 }
366 }
367};