blob: a846dbe9c1a4cb920069011683283da3be0003d3 [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 *
Robert Gindaca5b2872015-06-15 14:24:52 -070047 * A key sequence is zero or more of the key modifiers defined in
48 * hterm.Parser.identifiers.modifierKeys followed by a key code. Key
49 * codes can be an integer or an identifier from
50 * hterm.Parser.identifiers.keyCodes. Modifiers and keyCodes should be joined
51 * by the dash character.
Robert Gindaf82267d2015-06-09 15:32:04 -070052 *
Robert Gindaca5b2872015-06-15 14:24:52 -070053 * An asterisk "*" can be used to indicate that the unspecified modifiers
54 * are optional.
Robert Gindaf82267d2015-06-09 15:32:04 -070055 *
Robert Gindaca5b2872015-06-15 14:24:52 -070056 * For example:
57 * A: Matches only an unmodified "A" character.
58 * 65: Same as above.
59 * 0x41: Same as above.
60 * Ctrl-A: Matches only Ctrl-A.
61 * Ctrl-65: Same as above.
62 * Ctrl-0x41: Same as above.
63 * Ctrl-Shift-A: Matches only Ctrl-Shift-A.
64 * Ctrl-*-A: Matches Ctrl-A, as well as any other key sequence that includes
65 * at least the Ctrl and A keys.
66 *
67 * @return {Object} An object with shift, ctrl, alt, meta, keyCode
Robert Gindaf82267d2015-06-09 15:32:04 -070068 * properties.
69 */
70hterm.Parser.prototype.parseKeySequence = function() {
71 var rv = {
Robert Gindaf82267d2015-06-09 15:32:04 -070072 keyCode: null
73 };
74
Robert Gindaca5b2872015-06-15 14:24:52 -070075 for (var k in hterm.Parser.identifiers.modifierKeys) {
76 rv[hterm.Parser.identifiers.modifierKeys[k]] = false;
77 }
78
Robert Gindaf82267d2015-06-09 15:32:04 -070079 while (this.pos < this.source.length) {
80 this.skipSpace();
81
82 var token = this.parseToken();
83 if (token.type == 'integer') {
84 rv.keyCode = token.value;
85
86 } else if (token.type == 'identifier') {
Robert Gindaca5b2872015-06-15 14:24:52 -070087 if (token.value in hterm.Parser.identifiers.modifierKeys) {
88 var mod = hterm.Parser.identifiers.modifierKeys[token.value];
Robert Gindaf82267d2015-06-09 15:32:04 -070089 if (rv[mod] && rv[mod] != '*')
90 throw this.error('Duplicate modifier: ' + token.value);
91 rv[mod] = true;
92
Robert Gindaca5b2872015-06-15 14:24:52 -070093 } else if (token.value in hterm.Parser.identifiers.keyCodes) {
94 rv.keyCode = hterm.Parser.identifiers.keyCodes[token.value];
Robert Gindaf82267d2015-06-09 15:32:04 -070095
96 } else {
97 throw this.error('Unknown key: ' + token.value);
98 }
99
100 } else if (token.type == 'symbol') {
101 if (token.value == '*') {
Robert Gindaca5b2872015-06-15 14:24:52 -0700102 for (var id in hterm.Parser.identifiers.modifierKeys) {
103 var p = hterm.Parser.identifiers.modifierKeys[id];
Robert Gindaf82267d2015-06-09 15:32:04 -0700104 if (!rv[p])
105 rv[p] = '*';
Robert Gindaca5b2872015-06-15 14:24:52 -0700106 }
Robert Gindaf82267d2015-06-09 15:32:04 -0700107 } else {
108 throw this.error('Unexpected symbol: ' + token.value);
109 }
110 } else {
111 throw this.error('Expected integer or identifier');
112 }
113
114 this.skipSpace();
115
116 if (this.ch != '-')
117 break;
118
119 if (rv.keyCode != null)
120 throw this.error('Extra definition after target key');
121
122 this.advance(1);
123 }
124
125 if (rv.keyCode == null)
126 throw this.error('Missing target key');
127
128 return rv;
129};
130
131hterm.Parser.prototype.parseKeyAction = function() {
132 this.skipSpace();
133
134 var token = this.parseToken();
135
136 if (token.type == 'string')
137 return token.value;
138
139 if (token.type == 'identifier') {
140 if (token.value in hterm.Parser.identifiers.actions)
141 return hterm.Parser.identifiers.actions[token.value];
142
143 throw this.error('Unknown key action: ' + token.value);
144 }
145
146 throw this.error('Expected string or identifier');
147
148};
149
150hterm.Parser.prototype.peekString = function() {
151 return this.ch == '\'' || this.ch == '"';
152};
153
154hterm.Parser.prototype.peekIdentifier = function() {
155 return this.ch.match(/[a-z_]/i);
156};
157
158hterm.Parser.prototype.peekInteger = function() {
159 return this.ch.match(/[0-9]/);
160};
161
162hterm.Parser.prototype.parseToken = function() {
163 if (this.ch == '*') {
164 var rv = {type: 'symbol', value: this.ch};
165 this.advance(1);
166 return rv;
167 }
168
169 if (this.peekIdentifier())
170 return {type: 'identifier', value: this.parseIdentifier()};
171
172 if (this.peekString())
173 return {type: 'string', value: this.parseString()};
174
175 if (this.peekInteger())
176 return {type: 'integer', value: this.parseInteger()};
177
178
179 throw this.error('Unexpected token');
180};
181
182hterm.Parser.prototype.parseIdentifier = function() {
183 if (!this.peekIdentifier())
184 throw this.error('Expected identifier');
185
186 return this.parsePattern(/[a-z0-9_]+/ig);
187};
188
189hterm.Parser.prototype.parseInteger = function() {
190 var base = 10;
191
192 if (this.ch == '0' && this.pos < this.source.length - 1 &&
193 this.source.substr(this.pos + 1, 1) == 'x') {
194 return parseInt(this.parsePattern(/0x[0-9a-f]+/gi));
195 }
196
197 return parseInt(this.parsePattern(/\d+/g));
198};
199
200/**
201 * Parse a single or double quoted string.
202 *
203 * The current position should point at the initial quote character. Single
204 * quoted strings will be treated literally, double quoted will process escapes.
205 *
206 * TODO(rginda): Variable interpolation.
207 *
208 * @param {ParseState} parseState
209 * @param {string} quote A single or double-quote character.
210 * @return {string}
211 */
212hterm.Parser.prototype.parseString = function() {
213 var result = '';
214
215 var quote = this.ch;
216 if (quote != '"' && quote != '\'')
217 throw this.error('String expected');
218
219 this.advance(1);
220
221 var re = new RegExp('[\\\\' + quote + ']', 'g');
222
223 while (this.pos < this.source.length) {
224 re.lastIndex = this.pos;
225 if (!re.exec(this.source))
226 throw this.error('Unterminated string literal');
227
228 result += this.source.substring(this.pos, re.lastIndex - 1);
229
230 this.advance(re.lastIndex - this.pos - 1);
231
232 if (quote == '"' && this.ch == '\\') {
233 this.advance(1);
234 result += this.parseEscape();
235 continue;
236 }
237
238 if (quote == '\'' && this.ch == '\\') {
239 result += this.ch;
240 this.advance(1);
241 continue;
242 }
243
244 if (this.ch == quote) {
245 this.advance(1);
246 return result;
247 }
248 }
249
250 throw this.error('Unterminated string literal');
251};
252
253
254/**
255 * Parse an escape code from the current position (which should point to
256 * the first character AFTER the leading backslash.)
257 *
258 * @return {string}
259 */
260hterm.Parser.prototype.parseEscape = function() {
261 var map = {
262 '"': '"',
263 '\'': '\'',
264 '\\': '\\',
265 'a': '\x07',
266 'b': '\x08',
267 'e': '\x1b',
268 'f': '\x0c',
269 'n': '\x0a',
270 'r': '\x0d',
271 't': '\x09',
272 'v': '\x0b',
273 'x': function() {
274 var value = this.parsePattern(/[a-z0-9]{2}/ig);
275 return String.fromCharCode(parseInt(value, 16));
276 },
277 'u': function() {
278 var value = this.parsePattern(/[a-z0-9]{4}/ig);
279 return String.fromCharCode(parseInt(value, 16));
280 }
281 };
282
283 if (!(this.ch in map))
284 throw this.error('Unknown escape: ' + this.ch);
285
286 var value = map[this.ch];
287 this.advance(1);
288
289 if (typeof value == 'function')
290 value = value.call(this);
291
292 return value;
293};
294
295/**
296 * Parse the given pattern starting from the current position.
297 *
298 * @param {RegExp} pattern A pattern representing the characters to span. MUST
299 * include the "global" RegExp flag.
300 * @return {string}
301 */
302hterm.Parser.prototype.parsePattern = function(pattern) {
303 if (!pattern.global)
304 throw this.error('Internal error: Span patterns must be global');
305
306 pattern.lastIndex = this.pos;
307 var ary = pattern.exec(this.source);
308
309 if (!ary || pattern.lastIndex - ary[0].length != this.pos)
310 throw this.error('Expected match for: ' + pattern);
311
312 this.pos = pattern.lastIndex - 1;
313 this.advance(1);
314
315 return ary[0];
316};
317
318
319/**
320 * Advance the current position.
321 *
322 * @param {number} count
323 */
324hterm.Parser.prototype.advance = function(count) {
325 this.pos += count;
326 this.ch = this.source.substr(this.pos, 1);
327};
328
329/**
330 * @param {string=} opt_expect A list of valid non-whitespace characters to
331 * terminate on.
332 * @return {void}
333 */
334hterm.Parser.prototype.skipSpace = function(opt_expect) {
335 if (!/\s/.test(this.ch))
336 return;
337
338 var re = /\s+/gm;
339 re.lastIndex = this.pos;
340
341 var source = this.source;
342 if (re.exec(source))
343 this.pos = re.lastIndex;
344
345 this.ch = this.source.substr(this.pos, 1);
346
347 if (opt_expect) {
348 if (this.ch.indexOf(opt_expect) == -1) {
349 throw this.error('Expected one of ' + opt_expect + ', found: ' +
350 this.ch);
351 }
352 }
353};