blob: e2dfb94bd46e4f906e5f92beb75b6f2538823f6e [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
Mike Frysingerec4225d2020-04-07 05:00:01 -040044 * @param {number=} pos
Joel Hockey0f933582019-08-27 18:01:51 -070045 */
Mike Frysingerec4225d2020-04-07 05:00:01 -040046hterm.Parser.prototype.reset = function(source, pos = 0) {
Robert Gindaf82267d2015-06-09 15:32:04 -070047 this.source = source;
Mike Frysingerec4225d2020-04-07 05:00:01 -040048 this.pos = pos;
Robert Gindaf82267d2015-06-09 15:32:04 -070049 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];
Mike Frysingerbdb34802020-04-07 03:47:32 -040099 if (rv[mod] && rv[mod] != '*') {
Robert Gindaf82267d2015-06-09 15:32:04 -0700100 throw this.error('Duplicate modifier: ' + token.value);
Mike Frysingerbdb34802020-04-07 03:47:32 -0400101 }
Robert Gindaf82267d2015-06-09 15:32:04 -0700102 rv[mod] = true;
103
Mike Frysingere6721672017-05-26 01:26:55 -0400104 } else if (ucValue in hterm.Parser.identifiers.keyCodes &&
105 hterm.Parser.identifiers.keyCodes.hasOwnProperty(ucValue)) {
106 rv.keyCode = hterm.Parser.identifiers.keyCodes[ucValue];
Robert Gindaf82267d2015-06-09 15:32:04 -0700107
108 } else {
109 throw this.error('Unknown key: ' + token.value);
110 }
111
112 } else if (token.type == 'symbol') {
113 if (token.value == '*') {
Robert Gindaca5b2872015-06-15 14:24:52 -0700114 for (var id in hterm.Parser.identifiers.modifierKeys) {
115 var p = hterm.Parser.identifiers.modifierKeys[id];
Mike Frysingerbdb34802020-04-07 03:47:32 -0400116 if (!rv[p]) {
Robert Gindaf82267d2015-06-09 15:32:04 -0700117 rv[p] = '*';
Mike Frysingerbdb34802020-04-07 03:47:32 -0400118 }
Robert Gindaca5b2872015-06-15 14:24:52 -0700119 }
Robert Gindaf82267d2015-06-09 15:32:04 -0700120 } else {
121 throw this.error('Unexpected symbol: ' + token.value);
122 }
123 } else {
124 throw this.error('Expected integer or identifier');
125 }
126
127 this.skipSpace();
128
Mike Frysingerbdb34802020-04-07 03:47:32 -0400129 if (this.ch !== '-' && this.ch !== '+') {
Robert Gindaf82267d2015-06-09 15:32:04 -0700130 break;
Mike Frysingerbdb34802020-04-07 03:47:32 -0400131 }
Robert Gindaf82267d2015-06-09 15:32:04 -0700132
Mike Frysingerbdb34802020-04-07 03:47:32 -0400133 if (rv.keyCode != null) {
Robert Gindaf82267d2015-06-09 15:32:04 -0700134 throw this.error('Extra definition after target key');
Mike Frysingerbdb34802020-04-07 03:47:32 -0400135 }
Robert Gindaf82267d2015-06-09 15:32:04 -0700136
137 this.advance(1);
138 }
139
Mike Frysingerbdb34802020-04-07 03:47:32 -0400140 if (rv.keyCode == null) {
Robert Gindaf82267d2015-06-09 15:32:04 -0700141 throw this.error('Missing target key');
Mike Frysingerbdb34802020-04-07 03:47:32 -0400142 }
Robert Gindaf82267d2015-06-09 15:32:04 -0700143
144 return rv;
145};
146
Joel Hockey0f933582019-08-27 18:01:51 -0700147/** @return {string} */
Robert Gindaf82267d2015-06-09 15:32:04 -0700148hterm.Parser.prototype.parseKeyAction = function() {
149 this.skipSpace();
150
151 var token = this.parseToken();
152
Mike Frysingerbdb34802020-04-07 03:47:32 -0400153 if (token.type == 'string') {
Robert Gindaf82267d2015-06-09 15:32:04 -0700154 return token.value;
Mike Frysingerbdb34802020-04-07 03:47:32 -0400155 }
Robert Gindaf82267d2015-06-09 15:32:04 -0700156
157 if (token.type == 'identifier') {
Mike Frysinger9837fff2017-05-26 00:40:41 -0400158 if (token.value in hterm.Parser.identifiers.actions &&
Mike Frysingerbdb34802020-04-07 03:47:32 -0400159 hterm.Parser.identifiers.actions.hasOwnProperty(token.value)) {
Robert Gindaf82267d2015-06-09 15:32:04 -0700160 return hterm.Parser.identifiers.actions[token.value];
Mike Frysingerbdb34802020-04-07 03:47:32 -0400161 }
Robert Gindaf82267d2015-06-09 15:32:04 -0700162
163 throw this.error('Unknown key action: ' + token.value);
164 }
165
166 throw this.error('Expected string or identifier');
167
168};
169
Joel Hockey0f933582019-08-27 18:01:51 -0700170/** @return {boolean} */
Robert Gindaf82267d2015-06-09 15:32:04 -0700171hterm.Parser.prototype.peekString = function() {
172 return this.ch == '\'' || this.ch == '"';
173};
174
Joel Hockey0f933582019-08-27 18:01:51 -0700175/** @return {boolean} */
Robert Gindaf82267d2015-06-09 15:32:04 -0700176hterm.Parser.prototype.peekIdentifier = function() {
Joel Hockeyc98a0ce2019-09-20 16:26:20 -0700177 return !!this.ch.match(/[a-z_]/i);
Robert Gindaf82267d2015-06-09 15:32:04 -0700178};
179
Joel Hockey0f933582019-08-27 18:01:51 -0700180/** @return {boolean} */
Robert Gindaf82267d2015-06-09 15:32:04 -0700181hterm.Parser.prototype.peekInteger = function() {
Joel Hockeyc98a0ce2019-09-20 16:26:20 -0700182 return !!this.ch.match(/[0-9]/);
Robert Gindaf82267d2015-06-09 15:32:04 -0700183};
184
Joel Hockey0f933582019-08-27 18:01:51 -0700185/** @return {!Object} */
Robert Gindaf82267d2015-06-09 15:32:04 -0700186hterm.Parser.prototype.parseToken = function() {
187 if (this.ch == '*') {
188 var rv = {type: 'symbol', value: this.ch};
189 this.advance(1);
190 return rv;
191 }
192
Mike Frysingerbdb34802020-04-07 03:47:32 -0400193 if (this.peekIdentifier()) {
Robert Gindaf82267d2015-06-09 15:32:04 -0700194 return {type: 'identifier', value: this.parseIdentifier()};
Mike Frysingerbdb34802020-04-07 03:47:32 -0400195 }
Robert Gindaf82267d2015-06-09 15:32:04 -0700196
Mike Frysingerbdb34802020-04-07 03:47:32 -0400197 if (this.peekString()) {
Robert Gindaf82267d2015-06-09 15:32:04 -0700198 return {type: 'string', value: this.parseString()};
Mike Frysingerbdb34802020-04-07 03:47:32 -0400199 }
Robert Gindaf82267d2015-06-09 15:32:04 -0700200
Mike Frysingerbdb34802020-04-07 03:47:32 -0400201 if (this.peekInteger()) {
Robert Gindaf82267d2015-06-09 15:32:04 -0700202 return {type: 'integer', value: this.parseInteger()};
Mike Frysingerbdb34802020-04-07 03:47:32 -0400203 }
Robert Gindaf82267d2015-06-09 15:32:04 -0700204
205 throw this.error('Unexpected token');
206};
207
Joel Hockey0f933582019-08-27 18:01:51 -0700208/** @return {string} */
Robert Gindaf82267d2015-06-09 15:32:04 -0700209hterm.Parser.prototype.parseIdentifier = function() {
Mike Frysingerbdb34802020-04-07 03:47:32 -0400210 if (!this.peekIdentifier()) {
Robert Gindaf82267d2015-06-09 15:32:04 -0700211 throw this.error('Expected identifier');
Mike Frysingerbdb34802020-04-07 03:47:32 -0400212 }
Robert Gindaf82267d2015-06-09 15:32:04 -0700213
214 return this.parsePattern(/[a-z0-9_]+/ig);
215};
216
Joel Hockey0f933582019-08-27 18:01:51 -0700217/** @return {number} */
Robert Gindaf82267d2015-06-09 15:32:04 -0700218hterm.Parser.prototype.parseInteger = function() {
Robert Gindaf82267d2015-06-09 15:32:04 -0700219 if (this.ch == '0' && this.pos < this.source.length - 1 &&
220 this.source.substr(this.pos + 1, 1) == 'x') {
Mike Frysingerd9678882020-04-07 19:24:40 -0400221 /* eslint-disable radix */
Joel Hockeyc98a0ce2019-09-20 16:26:20 -0700222 return parseInt(this.parsePattern(/0x[0-9a-f]+/gi), undefined);
Robert Gindaf82267d2015-06-09 15:32:04 -0700223 }
224
Joel Hockeyc98a0ce2019-09-20 16:26:20 -0700225 return parseInt(this.parsePattern(/\d+/g), 10);
Robert Gindaf82267d2015-06-09 15:32:04 -0700226};
227
228/**
229 * Parse a single or double quoted string.
230 *
231 * The current position should point at the initial quote character. Single
232 * quoted strings will be treated literally, double quoted will process escapes.
233 *
234 * TODO(rginda): Variable interpolation.
235 *
Robert Gindaf82267d2015-06-09 15:32:04 -0700236 * @return {string}
237 */
238hterm.Parser.prototype.parseString = function() {
239 var result = '';
240
241 var quote = this.ch;
Mike Frysingerbdb34802020-04-07 03:47:32 -0400242 if (quote != '"' && quote != '\'') {
Robert Gindaf82267d2015-06-09 15:32:04 -0700243 throw this.error('String expected');
Mike Frysingerbdb34802020-04-07 03:47:32 -0400244 }
Robert Gindaf82267d2015-06-09 15:32:04 -0700245
246 this.advance(1);
247
248 var re = new RegExp('[\\\\' + quote + ']', 'g');
249
250 while (this.pos < this.source.length) {
251 re.lastIndex = this.pos;
Mike Frysingerbdb34802020-04-07 03:47:32 -0400252 if (!re.exec(this.source)) {
Robert Gindaf82267d2015-06-09 15:32:04 -0700253 throw this.error('Unterminated string literal');
Mike Frysingerbdb34802020-04-07 03:47:32 -0400254 }
Robert Gindaf82267d2015-06-09 15:32:04 -0700255
256 result += this.source.substring(this.pos, re.lastIndex - 1);
257
258 this.advance(re.lastIndex - this.pos - 1);
259
260 if (quote == '"' && this.ch == '\\') {
261 this.advance(1);
262 result += this.parseEscape();
263 continue;
264 }
265
266 if (quote == '\'' && this.ch == '\\') {
267 result += this.ch;
268 this.advance(1);
269 continue;
270 }
271
272 if (this.ch == quote) {
273 this.advance(1);
274 return result;
275 }
276 }
277
278 throw this.error('Unterminated string literal');
279};
280
281
282/**
283 * Parse an escape code from the current position (which should point to
284 * the first character AFTER the leading backslash.)
285 *
286 * @return {string}
287 */
288hterm.Parser.prototype.parseEscape = function() {
289 var map = {
290 '"': '"',
291 '\'': '\'',
292 '\\': '\\',
293 'a': '\x07',
294 'b': '\x08',
295 'e': '\x1b',
296 'f': '\x0c',
297 'n': '\x0a',
298 'r': '\x0d',
299 't': '\x09',
300 'v': '\x0b',
301 'x': function() {
302 var value = this.parsePattern(/[a-z0-9]{2}/ig);
303 return String.fromCharCode(parseInt(value, 16));
304 },
305 'u': function() {
306 var value = this.parsePattern(/[a-z0-9]{4}/ig);
307 return String.fromCharCode(parseInt(value, 16));
308 }
309 };
310
Mike Frysingerbdb34802020-04-07 03:47:32 -0400311 if (!(this.ch in map && map.hasOwnProperty(this.ch))) {
Robert Gindaf82267d2015-06-09 15:32:04 -0700312 throw this.error('Unknown escape: ' + this.ch);
Mike Frysingerbdb34802020-04-07 03:47:32 -0400313 }
Robert Gindaf82267d2015-06-09 15:32:04 -0700314
315 var value = map[this.ch];
316 this.advance(1);
317
Mike Frysingerbdb34802020-04-07 03:47:32 -0400318 if (typeof value == 'function') {
Robert Gindaf82267d2015-06-09 15:32:04 -0700319 value = value.call(this);
Mike Frysingerbdb34802020-04-07 03:47:32 -0400320 }
Robert Gindaf82267d2015-06-09 15:32:04 -0700321
322 return value;
323};
324
325/**
326 * Parse the given pattern starting from the current position.
327 *
Joel Hockey0f933582019-08-27 18:01:51 -0700328 * @param {!RegExp} pattern A pattern representing the characters to span. MUST
Robert Gindaf82267d2015-06-09 15:32:04 -0700329 * include the "global" RegExp flag.
330 * @return {string}
331 */
332hterm.Parser.prototype.parsePattern = function(pattern) {
Mike Frysingerbdb34802020-04-07 03:47:32 -0400333 if (!pattern.global) {
Robert Gindaf82267d2015-06-09 15:32:04 -0700334 throw this.error('Internal error: Span patterns must be global');
Mike Frysingerbdb34802020-04-07 03:47:32 -0400335 }
Robert Gindaf82267d2015-06-09 15:32:04 -0700336
337 pattern.lastIndex = this.pos;
338 var ary = pattern.exec(this.source);
339
Mike Frysingerbdb34802020-04-07 03:47:32 -0400340 if (!ary || pattern.lastIndex - ary[0].length != this.pos) {
Robert Gindaf82267d2015-06-09 15:32:04 -0700341 throw this.error('Expected match for: ' + pattern);
Mike Frysingerbdb34802020-04-07 03:47:32 -0400342 }
Robert Gindaf82267d2015-06-09 15:32:04 -0700343
344 this.pos = pattern.lastIndex - 1;
345 this.advance(1);
346
347 return ary[0];
348};
349
350
351/**
352 * Advance the current position.
353 *
354 * @param {number} count
355 */
356hterm.Parser.prototype.advance = function(count) {
357 this.pos += count;
358 this.ch = this.source.substr(this.pos, 1);
359};
360
361/**
Mike Frysingerec4225d2020-04-07 05:00:01 -0400362 * @param {string=} expect A list of valid non-whitespace characters to
Robert Gindaf82267d2015-06-09 15:32:04 -0700363 * terminate on.
364 * @return {void}
365 */
Mike Frysingerec4225d2020-04-07 05:00:01 -0400366hterm.Parser.prototype.skipSpace = function(expect = undefined) {
Mike Frysingerbdb34802020-04-07 03:47:32 -0400367 if (!/\s/.test(this.ch)) {
Robert Gindaf82267d2015-06-09 15:32:04 -0700368 return;
Mike Frysingerbdb34802020-04-07 03:47:32 -0400369 }
Robert Gindaf82267d2015-06-09 15:32:04 -0700370
371 var re = /\s+/gm;
372 re.lastIndex = this.pos;
373
374 var source = this.source;
Mike Frysingerbdb34802020-04-07 03:47:32 -0400375 if (re.exec(source)) {
Robert Gindaf82267d2015-06-09 15:32:04 -0700376 this.pos = re.lastIndex;
Mike Frysingerbdb34802020-04-07 03:47:32 -0400377 }
Robert Gindaf82267d2015-06-09 15:32:04 -0700378
379 this.ch = this.source.substr(this.pos, 1);
380
Mike Frysingerec4225d2020-04-07 05:00:01 -0400381 if (expect) {
382 if (this.ch.indexOf(expect) == -1) {
383 throw this.error(`Expected one of ${expect}, found: ${this.ch}`);
Robert Gindaf82267d2015-06-09 15:32:04 -0700384 }
385 }
386};