blob: afe846c6c6ac5e795d4239b90e3b8d7f53791fd6 [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.
Mike Frysinger23b5b832019-10-01 17:05:29 -04009 *
Joel Hockey0f933582019-08-27 18:01:51 -070010 * @constructor
Robert Gindaf82267d2015-06-09 15:32:04 -070011 */
12hterm.Parser = function() {
13 /**
14 * @type {string} The source string.
15 */
16 this.source = '';
17
18 /**
19 * @type {number} The current position.
20 */
21 this.pos = 0;
22
23 /**
Joel Hockey0f933582019-08-27 18:01:51 -070024 * @type {?string} The character at the current position.
Robert Gindaf82267d2015-06-09 15:32:04 -070025 */
26 this.ch = null;
27};
28
Joel Hockey0f933582019-08-27 18:01:51 -070029/**
30 * @param {string} message
31 * @return {!Error}
32 */
Robert Gindaf82267d2015-06-09 15:32:04 -070033hterm.Parser.prototype.error = function(message) {
34 return new Error('Parse error at ' + this.pos + ': ' + message);
35};
36
Joel Hockey0f933582019-08-27 18:01:51 -070037/** @return {boolean} */
Robert Gindaf82267d2015-06-09 15:32:04 -070038hterm.Parser.prototype.isComplete = function() {
39 return this.pos == this.source.length;
40};
41
Joel Hockey0f933582019-08-27 18:01:51 -070042/**
43 * @param {string} source
44 * @param {number=} opt_pos
45 */
Robert Gindaf82267d2015-06-09 15:32:04 -070046hterm.Parser.prototype.reset = function(source, opt_pos) {
47 this.source = source;
48 this.pos = opt_pos || 0;
49 this.ch = source.substr(0, 1);
50};
51
52/**
53 * Parse a key sequence.
54 *
Robert Gindaca5b2872015-06-15 14:24:52 -070055 * A key sequence is zero or more of the key modifiers defined in
56 * hterm.Parser.identifiers.modifierKeys followed by a key code. Key
57 * codes can be an integer or an identifier from
58 * hterm.Parser.identifiers.keyCodes. Modifiers and keyCodes should be joined
59 * by the dash character.
Robert Gindaf82267d2015-06-09 15:32:04 -070060 *
Robert Gindaca5b2872015-06-15 14:24:52 -070061 * An asterisk "*" can be used to indicate that the unspecified modifiers
62 * are optional.
Robert Gindaf82267d2015-06-09 15:32:04 -070063 *
Robert Gindaca5b2872015-06-15 14:24:52 -070064 * For example:
65 * A: Matches only an unmodified "A" character.
66 * 65: Same as above.
67 * 0x41: Same as above.
Joel Hockey46a6e1d2020-03-11 20:01:57 -070068 * Ctrl+A: Matches only Ctrl+A.
69 * Ctrl+65: Same as above.
70 * Ctrl+0x41: Same as above.
71 * Ctrl+Shift+A: Matches only Ctrl+Shift+A.
72 * Ctrl+*+A: Matches Ctrl+A, as well as any other key sequence that includes
Robert Gindaca5b2872015-06-15 14:24:52 -070073 * at least the Ctrl and A keys.
74 *
Joel Hockey0f933582019-08-27 18:01:51 -070075 * @return {!hterm.Keyboard.KeyDown} An object with shift, ctrl, alt, meta,
76 * keyCode properties.
Robert Gindaf82267d2015-06-09 15:32:04 -070077 */
78hterm.Parser.prototype.parseKeySequence = function() {
79 var rv = {
Robert Gindaf82267d2015-06-09 15:32:04 -070080 keyCode: null
81 };
82
Robert Gindaca5b2872015-06-15 14:24:52 -070083 for (var k in hterm.Parser.identifiers.modifierKeys) {
84 rv[hterm.Parser.identifiers.modifierKeys[k]] = false;
85 }
86
Robert Gindaf82267d2015-06-09 15:32:04 -070087 while (this.pos < this.source.length) {
88 this.skipSpace();
89
90 var token = this.parseToken();
91 if (token.type == 'integer') {
92 rv.keyCode = token.value;
93
94 } else if (token.type == 'identifier') {
Mike Frysingere6721672017-05-26 01:26:55 -040095 var ucValue = token.value.toUpperCase();
96 if (ucValue in hterm.Parser.identifiers.modifierKeys &&
97 hterm.Parser.identifiers.modifierKeys.hasOwnProperty(ucValue)) {
98 var mod = hterm.Parser.identifiers.modifierKeys[ucValue];
Robert Gindaf82267d2015-06-09 15:32:04 -070099 if (rv[mod] && rv[mod] != '*')
100 throw this.error('Duplicate modifier: ' + token.value);
101 rv[mod] = true;
102
Mike Frysingere6721672017-05-26 01:26:55 -0400103 } else if (ucValue in hterm.Parser.identifiers.keyCodes &&
104 hterm.Parser.identifiers.keyCodes.hasOwnProperty(ucValue)) {
105 rv.keyCode = hterm.Parser.identifiers.keyCodes[ucValue];
Robert Gindaf82267d2015-06-09 15:32:04 -0700106
107 } else {
108 throw this.error('Unknown key: ' + token.value);
109 }
110
111 } else if (token.type == 'symbol') {
112 if (token.value == '*') {
Robert Gindaca5b2872015-06-15 14:24:52 -0700113 for (var id in hterm.Parser.identifiers.modifierKeys) {
114 var p = hterm.Parser.identifiers.modifierKeys[id];
Robert Gindaf82267d2015-06-09 15:32:04 -0700115 if (!rv[p])
116 rv[p] = '*';
Robert Gindaca5b2872015-06-15 14:24:52 -0700117 }
Robert Gindaf82267d2015-06-09 15:32:04 -0700118 } else {
119 throw this.error('Unexpected symbol: ' + token.value);
120 }
121 } else {
122 throw this.error('Expected integer or identifier');
123 }
124
125 this.skipSpace();
126
Joel Hockey46a6e1d2020-03-11 20:01:57 -0700127 if (this.ch !== '-' && this.ch !== '+')
Robert Gindaf82267d2015-06-09 15:32:04 -0700128 break;
129
130 if (rv.keyCode != null)
131 throw this.error('Extra definition after target key');
132
133 this.advance(1);
134 }
135
136 if (rv.keyCode == null)
137 throw this.error('Missing target key');
138
139 return rv;
140};
141
Joel Hockey0f933582019-08-27 18:01:51 -0700142/** @return {string} */
Robert Gindaf82267d2015-06-09 15:32:04 -0700143hterm.Parser.prototype.parseKeyAction = function() {
144 this.skipSpace();
145
146 var token = this.parseToken();
147
148 if (token.type == 'string')
149 return token.value;
150
151 if (token.type == 'identifier') {
Mike Frysinger9837fff2017-05-26 00:40:41 -0400152 if (token.value in hterm.Parser.identifiers.actions &&
153 hterm.Parser.identifiers.actions.hasOwnProperty(token.value))
Robert Gindaf82267d2015-06-09 15:32:04 -0700154 return hterm.Parser.identifiers.actions[token.value];
155
156 throw this.error('Unknown key action: ' + token.value);
157 }
158
159 throw this.error('Expected string or identifier');
160
161};
162
Joel Hockey0f933582019-08-27 18:01:51 -0700163/** @return {boolean} */
Robert Gindaf82267d2015-06-09 15:32:04 -0700164hterm.Parser.prototype.peekString = function() {
165 return this.ch == '\'' || this.ch == '"';
166};
167
Joel Hockey0f933582019-08-27 18:01:51 -0700168/** @return {boolean} */
Robert Gindaf82267d2015-06-09 15:32:04 -0700169hterm.Parser.prototype.peekIdentifier = function() {
Joel Hockeyc98a0ce2019-09-20 16:26:20 -0700170 return !!this.ch.match(/[a-z_]/i);
Robert Gindaf82267d2015-06-09 15:32:04 -0700171};
172
Joel Hockey0f933582019-08-27 18:01:51 -0700173/** @return {boolean} */
Robert Gindaf82267d2015-06-09 15:32:04 -0700174hterm.Parser.prototype.peekInteger = function() {
Joel Hockeyc98a0ce2019-09-20 16:26:20 -0700175 return !!this.ch.match(/[0-9]/);
Robert Gindaf82267d2015-06-09 15:32:04 -0700176};
177
Joel Hockey0f933582019-08-27 18:01:51 -0700178/** @return {!Object} */
Robert Gindaf82267d2015-06-09 15:32:04 -0700179hterm.Parser.prototype.parseToken = function() {
180 if (this.ch == '*') {
181 var rv = {type: 'symbol', value: this.ch};
182 this.advance(1);
183 return rv;
184 }
185
186 if (this.peekIdentifier())
187 return {type: 'identifier', value: this.parseIdentifier()};
188
189 if (this.peekString())
190 return {type: 'string', value: this.parseString()};
191
192 if (this.peekInteger())
193 return {type: 'integer', value: this.parseInteger()};
194
195
196 throw this.error('Unexpected token');
197};
198
Joel Hockey0f933582019-08-27 18:01:51 -0700199/** @return {string} */
Robert Gindaf82267d2015-06-09 15:32:04 -0700200hterm.Parser.prototype.parseIdentifier = function() {
201 if (!this.peekIdentifier())
202 throw this.error('Expected identifier');
203
204 return this.parsePattern(/[a-z0-9_]+/ig);
205};
206
Joel Hockey0f933582019-08-27 18:01:51 -0700207/** @return {number} */
Robert Gindaf82267d2015-06-09 15:32:04 -0700208hterm.Parser.prototype.parseInteger = function() {
Robert Gindaf82267d2015-06-09 15:32:04 -0700209 if (this.ch == '0' && this.pos < this.source.length - 1 &&
210 this.source.substr(this.pos + 1, 1) == 'x') {
Joel Hockeyc98a0ce2019-09-20 16:26:20 -0700211 return parseInt(this.parsePattern(/0x[0-9a-f]+/gi), undefined);
Robert Gindaf82267d2015-06-09 15:32:04 -0700212 }
213
Joel Hockeyc98a0ce2019-09-20 16:26:20 -0700214 return parseInt(this.parsePattern(/\d+/g), 10);
Robert Gindaf82267d2015-06-09 15:32:04 -0700215};
216
217/**
218 * Parse a single or double quoted string.
219 *
220 * The current position should point at the initial quote character. Single
221 * quoted strings will be treated literally, double quoted will process escapes.
222 *
223 * TODO(rginda): Variable interpolation.
224 *
Robert Gindaf82267d2015-06-09 15:32:04 -0700225 * @return {string}
226 */
227hterm.Parser.prototype.parseString = function() {
228 var result = '';
229
230 var quote = this.ch;
231 if (quote != '"' && quote != '\'')
232 throw this.error('String expected');
233
234 this.advance(1);
235
236 var re = new RegExp('[\\\\' + quote + ']', 'g');
237
238 while (this.pos < this.source.length) {
239 re.lastIndex = this.pos;
240 if (!re.exec(this.source))
241 throw this.error('Unterminated string literal');
242
243 result += this.source.substring(this.pos, re.lastIndex - 1);
244
245 this.advance(re.lastIndex - this.pos - 1);
246
247 if (quote == '"' && this.ch == '\\') {
248 this.advance(1);
249 result += this.parseEscape();
250 continue;
251 }
252
253 if (quote == '\'' && this.ch == '\\') {
254 result += this.ch;
255 this.advance(1);
256 continue;
257 }
258
259 if (this.ch == quote) {
260 this.advance(1);
261 return result;
262 }
263 }
264
265 throw this.error('Unterminated string literal');
266};
267
268
269/**
270 * Parse an escape code from the current position (which should point to
271 * the first character AFTER the leading backslash.)
272 *
273 * @return {string}
274 */
275hterm.Parser.prototype.parseEscape = function() {
276 var map = {
277 '"': '"',
278 '\'': '\'',
279 '\\': '\\',
280 'a': '\x07',
281 'b': '\x08',
282 'e': '\x1b',
283 'f': '\x0c',
284 'n': '\x0a',
285 'r': '\x0d',
286 't': '\x09',
287 'v': '\x0b',
288 'x': function() {
289 var value = this.parsePattern(/[a-z0-9]{2}/ig);
290 return String.fromCharCode(parseInt(value, 16));
291 },
292 'u': function() {
293 var value = this.parsePattern(/[a-z0-9]{4}/ig);
294 return String.fromCharCode(parseInt(value, 16));
295 }
296 };
297
Mike Frysinger9837fff2017-05-26 00:40:41 -0400298 if (!(this.ch in map && map.hasOwnProperty(this.ch)))
Robert Gindaf82267d2015-06-09 15:32:04 -0700299 throw this.error('Unknown escape: ' + this.ch);
300
301 var value = map[this.ch];
302 this.advance(1);
303
304 if (typeof value == 'function')
305 value = value.call(this);
306
307 return value;
308};
309
310/**
311 * Parse the given pattern starting from the current position.
312 *
Joel Hockey0f933582019-08-27 18:01:51 -0700313 * @param {!RegExp} pattern A pattern representing the characters to span. MUST
Robert Gindaf82267d2015-06-09 15:32:04 -0700314 * include the "global" RegExp flag.
315 * @return {string}
316 */
317hterm.Parser.prototype.parsePattern = function(pattern) {
318 if (!pattern.global)
319 throw this.error('Internal error: Span patterns must be global');
320
321 pattern.lastIndex = this.pos;
322 var ary = pattern.exec(this.source);
323
324 if (!ary || pattern.lastIndex - ary[0].length != this.pos)
325 throw this.error('Expected match for: ' + pattern);
326
327 this.pos = pattern.lastIndex - 1;
328 this.advance(1);
329
330 return ary[0];
331};
332
333
334/**
335 * Advance the current position.
336 *
337 * @param {number} count
338 */
339hterm.Parser.prototype.advance = function(count) {
340 this.pos += count;
341 this.ch = this.source.substr(this.pos, 1);
342};
343
344/**
345 * @param {string=} opt_expect A list of valid non-whitespace characters to
346 * terminate on.
347 * @return {void}
348 */
349hterm.Parser.prototype.skipSpace = function(opt_expect) {
350 if (!/\s/.test(this.ch))
351 return;
352
353 var re = /\s+/gm;
354 re.lastIndex = this.pos;
355
356 var source = this.source;
357 if (re.exec(source))
358 this.pos = re.lastIndex;
359
360 this.ch = this.source.substr(this.pos, 1);
361
362 if (opt_expect) {
363 if (this.ch.indexOf(opt_expect) == -1) {
364 throw this.error('Expected one of ' + opt_expect + ', found: ' +
365 this.ch);
366 }
367 }
368};