OLD | NEW |
1 // Copyright 2014 The Chromium Authors. All rights reserved. | 1 // Copyright 2014 The Chromium Authors. All rights reserved. |
2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
4 | 4 |
5 /** | 5 /** |
6 * @fileoverview Provides output services for ChromeVox. | 6 * @fileoverview Provides output services for ChromeVox. |
7 */ | 7 */ |
8 | 8 |
9 goog.provide('Output'); | 9 goog.provide('Output'); |
10 goog.provide('Output.EventType'); | 10 goog.provide('Output.EventType'); |
(...skipping 28 matching lines...) Expand all Loading... |
39 * @ prefix: used to substitute a message. Note the ability to specify params to | 39 * @ prefix: used to substitute a message. Note the ability to specify params to |
40 * the message. For example, '@tag_html' '@selected_index($text_sel_start, | 40 * the message. For example, '@tag_html' '@selected_index($text_sel_start, |
41 * $text_sel_end'). | 41 * $text_sel_end'). |
42 * = suffix: used to specify substitution only if not previously appended. | 42 * = suffix: used to specify substitution only if not previously appended. |
43 * For example, $name= would insert the name attribute only if no name | 43 * For example, $name= would insert the name attribute only if no name |
44 * attribute had been inserted previously. | 44 * attribute had been inserted previously. |
45 * @constructor | 45 * @constructor |
46 */ | 46 */ |
47 Output = function() { | 47 Output = function() { |
48 // TODO(dtseng): Include braille specific rules. | 48 // TODO(dtseng): Include braille specific rules. |
49 /** @type {!Array<cvox.Spannable>} */ | 49 /** @type {!cvox.Spannable} */ |
50 this.buffer_ = []; | 50 this.buffer_ = new cvox.Spannable(); |
51 /** @type {!Array<cvox.Spannable>} */ | 51 /** @type {!cvox.Spannable} */ |
52 this.brailleBuffer_ = []; | 52 this.brailleBuffer_ = new cvox.Spannable(); |
53 /** @type {!Array<Object>} */ | 53 /** @type {!Array<Object>} */ |
54 this.locations_ = []; | 54 this.locations_ = []; |
55 /** @type {function()} */ | 55 /** @type {function()} */ |
56 this.speechStartCallback_; | 56 this.speechStartCallback_; |
57 /** @type {function()} */ | 57 /** @type {function()} */ |
58 this.speechEndCallback_; | 58 this.speechEndCallback_; |
59 | 59 |
60 /** | 60 /** |
61 * Current global options. | 61 * Current global options. |
62 * @type {{speech: boolean, braille: boolean, location: boolean}} | 62 * @type {{speech: boolean, braille: boolean, location: boolean}} |
(...skipping 20 matching lines...) Expand all Loading... |
83 */ | 83 */ |
84 Output.ROLE_INFO_ = { | 84 Output.ROLE_INFO_ = { |
85 alert: { | 85 alert: { |
86 msgId: 'aria_role_alert', | 86 msgId: 'aria_role_alert', |
87 earcon: 'ALERT_NONMODAL', | 87 earcon: 'ALERT_NONMODAL', |
88 }, | 88 }, |
89 button: { | 89 button: { |
90 msgId: 'tag_button', | 90 msgId: 'tag_button', |
91 earcon: 'BUTTON' | 91 earcon: 'BUTTON' |
92 }, | 92 }, |
93 checkBox: { | 93 checkbox: { |
94 msgId: 'input_type_checkbox' | 94 msgId: 'input_type_checkbox' |
95 }, | 95 }, |
96 dialog: { | |
97 msgId: 'dialog' | |
98 }, | |
99 heading: { | 96 heading: { |
100 msgId: 'aria_role_heading', | 97 msgId: 'aria_role_heading', |
101 }, | 98 }, |
102 link: { | 99 link: { |
103 msgId: 'tag_link', | 100 msgId: 'tag_link', |
104 earcon: 'LINK' | 101 earcon: 'LINK' |
105 }, | 102 }, |
106 listItem: { | 103 listItem: { |
107 msgId: 'ARIA_ROLE_LISTITEM', | 104 msgId: 'ARIA_ROLE_LISTITEM', |
108 earcon: 'list_item' | 105 earcon: 'list_item' |
(...skipping 13 matching lines...) Expand all Loading... |
122 }, | 119 }, |
123 textField: { | 120 textField: { |
124 msgId: 'input_type_text', | 121 msgId: 'input_type_text', |
125 earcon: 'EDITABLE_TEXT' | 122 earcon: 'EDITABLE_TEXT' |
126 }, | 123 }, |
127 toolbar: { | 124 toolbar: { |
128 msgId: 'aria_role_toolbar' | 125 msgId: 'aria_role_toolbar' |
129 } | 126 } |
130 }; | 127 }; |
131 | 128 |
132 /** | |
133 * Metadata about supported automation states. | |
134 * @const {!Object<string, | |
135 * {on: {msgId: string, earconId: string}, | |
136 * off: {msgId: string, earconId: string}}>} | |
137 * @private | |
138 */ | |
139 Output.STATE_INFO_ = { | |
140 checked: { | |
141 on: { | |
142 earconId: 'CHECK_ON', | |
143 msgId: 'checkbox_checked_state' | |
144 }, | |
145 off: { | |
146 earconId: 'CHECK_OFF', | |
147 msgId: 'checkbox_unchecked_state' | |
148 } | |
149 } | |
150 }; | |
151 | |
152 /** | 129 /** |
153 * Rules specifying format of AutomationNodes for output. | 130 * Rules specifying format of AutomationNodes for output. |
154 * @type {!Object<string, Object<string, Object<string, string>>>} | 131 * @type {!Object<string, Object<string, Object<string, string>>>} |
155 */ | 132 */ |
156 Output.RULES = { | 133 Output.RULES = { |
157 navigate: { | 134 navigate: { |
158 'default': { | 135 'default': { |
159 speak: '$name $value $role', | 136 speak: '$name $value $role', |
160 braille: '' | 137 braille: '' |
161 }, | 138 }, |
162 alert: { | 139 alert: { |
163 speak: '!doNotInterrupt $role $descendants' | 140 speak: '!doNotInterrupt ' + |
| 141 '@aria_role_alert $name $earcon(ALERT_NONMODAL) $descendants' |
164 }, | 142 }, |
165 checkBox: { | 143 checkBox: { |
166 speak: '$name $role $checked' | 144 speak: '$if($checked, @describe_checkbox_checked($name), ' + |
| 145 '@describe_checkbox_unchecked($name)) ' + |
| 146 '$if($checked, ' + |
| 147 '$earcon(CHECK_ON, @input_type_checkbox), ' + |
| 148 '$earcon(CHECK_OFF, @input_type_checkbox))' |
167 }, | 149 }, |
168 dialog: { | 150 dialog: { |
169 enter: '$name $role' | 151 enter: '$name $role' |
170 }, | 152 }, |
171 heading: { | 153 heading: { |
172 enter: '@aria_role_heading', | 154 enter: '@aria_role_heading', |
173 speak: '@aria_role_heading $name=' | 155 speak: '@aria_role_heading $name=' |
174 }, | 156 }, |
175 inlineTextBox: { | 157 inlineTextBox: { |
176 speak: '$value=' | 158 speak: '$value=' |
177 }, | 159 }, |
178 link: { | 160 link: { |
179 enter: '$name $visited $role', | 161 enter: '$name= $visited $earcon(LINK, @tag_link)=', |
180 stay: '$name= $visited $role', | 162 stay: '$name= $visited @tag_link', |
181 speak: '$name= $visited $role' | 163 speak: '$name= $visited $earcon(LINK, @tag_link)=' |
182 }, | 164 }, |
183 list: { | 165 list: { |
184 enter: '@aria_role_list @list_with_items($parentChildCount)' | 166 enter: '@aria_role_list @list_with_items($parentChildCount)' |
185 }, | 167 }, |
186 listItem: { | 168 listItem: { |
187 enter: '$role' | 169 enter: '$role' |
188 }, | 170 }, |
189 menuItem: { | 171 menuItem: { |
190 speak: '$if($haspopup, @describe_menu_item_with_submenu($name), ' + | 172 speak: '$if($haspopup, @describe_menu_item_with_submenu($name), ' + |
191 '@describe_menu_item($name)) ' + | 173 '@describe_menu_item($name)) ' + |
192 '@describe_index($indexInParent, $parentChildCount)' | 174 '@describe_index($indexInParent, $parentChildCount)' |
193 }, | 175 }, |
194 menuListOption: { | 176 menuListOption: { |
195 speak: '$name $value @aria_role_menuitem ' + | 177 speak: '$name $value @aria_role_menuitem ' + |
196 '@describe_index($indexInParent, $parentChildCount)' | 178 '@describe_index($indexInParent, $parentChildCount)' |
197 }, | 179 }, |
198 paragraph: { | 180 paragraph: { |
199 speak: '$value' | 181 speak: '$value' |
200 }, | 182 }, |
201 popUpButton: { | 183 popUpButton: { |
202 speak: '$value $name $role @aria_has_popup ' + | 184 speak: '$value $name @tag_button @aria_has_popup $earcon(LISTBOX) ' + |
203 '$if($collapsed, @aria_expanded_false, @aria_expanded_true)' | 185 '$if($collapsed, @aria_expanded_false, @aria_expanded_true)' |
204 }, | 186 }, |
205 radioButton: { | 187 radioButton: { |
206 speak: '$if($checked, @describe_radio_selected($name), ' + | 188 speak: '$if($checked, @describe_radio_selected($name), ' + |
207 '@describe_radio_unselected($name))' | 189 '@describe_radio_unselected($name)) ' + |
| 190 '$if($checked, ' + |
| 191 '$earcon(CHECK_ON, @input_type_radio), ' + |
| 192 '$earcon(CHECK_OFF, @input_type_radio))' |
208 }, | 193 }, |
209 slider: { | 194 slider: { |
210 speak: '@describe_slider($value, $name)' | 195 speak: '@describe_slider($value, $name)' |
211 }, | 196 }, |
212 staticText: { | 197 staticText: { |
213 speak: '$value $name' | 198 speak: '$value $name' |
214 }, | 199 }, |
215 tab: { | 200 tab: { |
216 speak: '@describe_tab($name)' | 201 speak: '@describe_tab($name)' |
217 }, | 202 }, |
218 toolbar: { | 203 toolbar: { |
219 enter: '$name $role' | 204 enter: '$name $role' |
220 }, | 205 }, |
221 window: { | 206 window: { |
222 enter: '$name', | 207 enter: '$name', |
223 speak: '@describe_window($name) $earcon(OBJECT_OPEN)' | 208 speak: '@describe_window($name) $earcon(OBJECT_OPEN)' |
224 } | 209 } |
225 }, | 210 }, |
226 menuStart: { | 211 menuStart: { |
227 'default': { | 212 'default': { |
228 speak: '@chrome_menu_opened($name) $earcon(OBJECT_OPEN)' | 213 speak: '@chrome_menu_opened($name) $role $earcon(OBJECT_OPEN)' |
229 } | 214 } |
230 }, | 215 }, |
231 menuEnd: { | 216 menuEnd: { |
232 'default': { | 217 'default': { |
233 speak: '@chrome_menu_closed $earcon(OBJECT_CLOSE)' | 218 speak: '$earcon(OBJECT_CLOSE)' |
234 } | 219 } |
235 }, | 220 }, |
236 menuListValueChanged: { | 221 menuListValueChanged: { |
237 'default': { | 222 'default': { |
238 speak: '$value $name ' + | 223 speak: '$value $name ' + |
239 '$find({"state": {"selected": true, "invisible": false}}, ' + | 224 '$find({"state": {"selected": true, "invisible": false}}, ' + |
240 '@describe_index($indexInParent, $parentChildCount)) ' | 225 '@describe_index($indexInParent, $parentChildCount)) ' |
241 } | 226 } |
242 }, | 227 }, |
243 alert: { | 228 alert: { |
244 default: { | 229 default: { |
245 speak: '!doNotInterrupt ' + | 230 speak: '!doNotInterrupt ' + |
246 '@aria_role_alert $name $earcon(ALERT_NONMODAL) $descendants' | 231 '@aria_role_alert $name $earcon(ALERT_NONMODAL) $descendants' |
247 } | 232 } |
248 } | 233 } |
249 }; | 234 }; |
250 | 235 |
251 /** | 236 /** |
| 237 * Alias equivalent attributes. |
| 238 * @type {!Object<string, string>} |
| 239 */ |
| 240 Output.ATTRIBUTE_ALIAS = { |
| 241 name: 'value', |
| 242 value: 'name' |
| 243 }; |
| 244 |
| 245 /** |
252 * Custom actions performed while rendering an output string. | 246 * Custom actions performed while rendering an output string. |
253 * @param {function()} action | 247 * @param {function()} action |
254 * @constructor | 248 * @constructor |
255 */ | 249 */ |
256 Output.Action = function(action) { | 250 Output.Action = function(action) { |
257 this.action_ = action; | 251 this.action_ = action; |
258 }; | 252 }; |
259 | 253 |
260 Output.Action.prototype = { | 254 Output.Action.prototype = { |
261 run: function() { | 255 run: function() { |
262 this.action_(); | 256 this.action_(); |
263 } | 257 } |
264 }; | 258 }; |
265 | 259 |
266 /** | 260 /** |
267 * Action to play a earcon. | |
268 * @param {string} earconId | |
269 * @constructor | |
270 * @extends {Output.Action} | |
271 */ | |
272 Output.EarconAction = function(earconId) { | |
273 Output.Action.call(this, function() { | |
274 cvox.ChromeVox.earcons.playEarcon( | |
275 cvox.AbstractEarcons[earconId]); | |
276 }); | |
277 }; | |
278 | |
279 Output.EarconAction.prototype = { | |
280 __proto__: Output.Action.prototype | |
281 }; | |
282 | |
283 /** | |
284 * Annotation for selection. | 261 * Annotation for selection. |
285 * @param {number} startIndex | 262 * @param {number} startIndex |
286 * @param {number} endIndex | 263 * @param {number} endIndex |
287 * @constructor | 264 * @constructor |
288 */ | 265 */ |
289 Output.SelectionSpan = function(startIndex, endIndex) { | 266 Output.SelectionSpan = function(startIndex, endIndex) { |
290 // TODO(dtseng): Direction lost below; should preserve for braille panning. | 267 // TODO(dtseng): Direction lost below; should preserve for braille panning. |
291 this.startIndex = startIndex < endIndex ? startIndex : endIndex; | 268 this.startIndex = startIndex < endIndex ? startIndex : endIndex; |
292 this.endIndex = endIndex > startIndex ? endIndex : startIndex; | 269 this.endIndex = endIndex > startIndex ? endIndex : startIndex; |
293 }; | 270 }; |
294 | 271 |
295 /** | 272 /** |
296 * Possible events handled by ChromeVox internally. | 273 * Possible events handled by ChromeVox internally. |
297 * @enum {string} | 274 * @enum {string} |
298 */ | 275 */ |
299 Output.EventType = { | 276 Output.EventType = { |
300 NAVIGATE: 'navigate' | 277 NAVIGATE: 'navigate' |
301 }; | 278 }; |
302 | 279 |
303 Output.prototype = { | 280 Output.prototype = { |
304 /** | 281 /** |
305 * Gets the output buffer for speech. | 282 * Gets the output buffer for speech. |
306 * @return {!cvox.Spannable} | 283 * @return {!cvox.Spannable} |
307 */ | 284 */ |
308 toSpannable: function() { | 285 getBuffer: function() { |
309 return this.buffer_.reduce(function(prev, cur) { | 286 return this.buffer_; |
310 prev.append(cur); | |
311 return prev; | |
312 }, new cvox.Spannable()); | |
313 }, | 287 }, |
314 | 288 |
315 /** | 289 /** |
316 * Specify ranges for speech. | 290 * Specify ranges for speech. |
317 * @param {!cursors.Range} range | 291 * @param {!cursors.Range} range |
318 * @param {cursors.Range} prevRange | 292 * @param {cursors.Range} prevRange |
319 * @param {chrome.automation.EventType|Output.EventType} type | 293 * @param {chrome.automation.EventType|Output.EventType} type |
320 * @return {!Output} | 294 * @return {!Output} |
321 */ | 295 */ |
322 withSpeech: function(range, prevRange, type) { | 296 withSpeech: function(range, prevRange, type) { |
(...skipping 60 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
383 onSpeechEnd: function(callback) { | 357 onSpeechEnd: function(callback) { |
384 this.speechEndCallback_ = callback; | 358 this.speechEndCallback_ = callback; |
385 return this; | 359 return this; |
386 }, | 360 }, |
387 | 361 |
388 /** | 362 /** |
389 * Executes all specified output. | 363 * Executes all specified output. |
390 */ | 364 */ |
391 go: function() { | 365 go: function() { |
392 // Speech. | 366 // Speech. |
393 var queueMode = cvox.QueueMode.FLUSH; | 367 var buff = this.buffer_; |
394 this.buffer_.forEach(function(buff, i, a) { | 368 if (buff.toString()) { |
395 if (buff.toString()) { | 369 if (this.speechStartCallback_) |
396 if (this.speechStartCallback_ && i == 0) | 370 this.speechProperties_['startCallback'] = this.speechStartCallback_; |
397 this.speechProperties_['startCallback'] = this.speechStartCallback_; | 371 if (this.speechEndCallback_) { |
398 else | 372 this.speechProperties_['endCallback'] = this.speechEndCallback_; |
399 this.speechProperties_['startCallback'] = null; | |
400 if (this.speechEndCallback_ && i == a.length - 1) | |
401 this.speechProperties_['endCallback'] = this.speechEndCallback_; | |
402 else | |
403 this.speechProperties_['endCallback'] = null; | |
404 cvox.ChromeVox.tts.speak( | |
405 buff.toString(), queueMode, this.speechProperties_); | |
406 queueMode = cvox.QueueMode.QUEUE; | |
407 } | 373 } |
408 var actions = buff.getSpansInstanceOf(Output.Action); | 374 |
409 if (actions) { | 375 cvox.ChromeVox.tts.speak( |
410 actions.forEach(function(a) { | 376 buff.toString(), cvox.QueueMode.FLUSH, this.speechProperties_); |
411 a.run(); | 377 } |
412 }); | 378 |
413 } | 379 var actions = buff.getSpansInstanceOf(Output.Action); |
414 }.bind(this)); | 380 if (actions) { |
| 381 actions.forEach(function(a) { |
| 382 a.run(); |
| 383 }); |
| 384 } |
415 | 385 |
416 // Braille. | 386 // Braille. |
417 var buff = this.brailleBuffer_.reduce(function(prev, cur) { | |
418 if (prev.getLength() > 0 && cur.getLength() > 0) | |
419 prev.append(Output.SPACE); | |
420 prev.append(cur); | |
421 return prev; | |
422 }, new cvox.Spannable()); | |
423 | |
424 var selSpan = | 387 var selSpan = |
425 buff.getSpanInstanceOf(Output.SelectionSpan); | 388 this.brailleBuffer_.getSpanInstanceOf(Output.SelectionSpan); |
426 var startIndex = -1, endIndex = -1; | 389 var startIndex = -1, endIndex = -1; |
427 if (selSpan) { | 390 if (selSpan) { |
428 // Casts ok, since the span is known to be in the spannable. | 391 // Casts ok, since the span is known to be in the spannable. |
429 var valueStart = | 392 var valueStart = |
430 /** @type {number} */ (buff.getSpanStart(selSpan)); | 393 /** @type {number} */ (this.brailleBuffer_.getSpanStart(selSpan)); |
431 var valueEnd = | 394 var valueEnd = |
432 /** @type {number} */ (buff.getSpanEnd(selSpan)); | 395 /** @type {number} */ (this.brailleBuffer_.getSpanEnd(selSpan)); |
433 startIndex = valueStart + selSpan.startIndex; | 396 startIndex = valueStart + selSpan.startIndex; |
434 endIndex = valueStart + selSpan.endIndex; | 397 endIndex = valueStart + selSpan.endIndex; |
435 buff.setSpan(new cvox.ValueSpan(0), | 398 this.brailleBuffer_.setSpan(new cvox.ValueSpan(0), |
436 valueStart, valueEnd); | 399 valueStart, valueEnd); |
437 buff.setSpan(new cvox.ValueSelectionSpan(), | 400 this.brailleBuffer_.setSpan(new cvox.ValueSelectionSpan(), |
438 startIndex, endIndex); | 401 startIndex, endIndex); |
439 } | 402 } |
440 | 403 |
441 var output = new cvox.NavBraille({ | 404 var output = new cvox.NavBraille({ |
442 text: buff, | 405 text: this.brailleBuffer_, |
443 startIndex: startIndex, | 406 startIndex: startIndex, |
444 endIndex: endIndex | 407 endIndex: endIndex |
445 }); | 408 }); |
446 | 409 |
447 if (this.brailleBuffer_) | 410 if (this.brailleBuffer_) |
448 cvox.ChromeVox.braille.write(output); | 411 cvox.ChromeVox.braille.write(output); |
449 | 412 |
450 // Display. | 413 // Display. |
451 chrome.accessibilityPrivate.setFocusRing(this.locations_); | 414 chrome.accessibilityPrivate.setFocusRing(this.locations_); |
452 }, | 415 }, |
453 | 416 |
454 /** | 417 /** |
455 * Renders the given range using optional context previous range and event | 418 * Renders the given range using optional context previous range and event |
456 * type. | 419 * type. |
457 * @param {!cursors.Range} range | 420 * @param {!cursors.Range} range |
458 * @param {cursors.Range} prevRange | 421 * @param {cursors.Range} prevRange |
459 * @param {chrome.automation.EventType|string} type | 422 * @param {chrome.automation.EventType|string} type |
460 * @param {!Array<cvox.Spannable>} buff Buffer to receive rendered output. | 423 * @param {!cvox.Spannable} buff Buffer to receive rendered output. |
461 * @private | 424 * @private |
462 */ | 425 */ |
463 render_: function(range, prevRange, type, buff) { | 426 render_: function(range, prevRange, type, buff) { |
464 if (range.isSubNode()) | 427 if (range.isSubNode()) |
465 this.subNode_(range, prevRange, type, buff); | 428 this.subNode_(range, prevRange, type, buff); |
466 else | 429 else |
467 this.range_(range, prevRange, type, buff); | 430 this.range_(range, prevRange, type, buff); |
468 }, | 431 }, |
469 | 432 |
470 /** | 433 /** |
471 * Format the node given the format specifier. | 434 * Format the node given the format specifier. |
472 * @param {chrome.automation.AutomationNode} node | 435 * @param {chrome.automation.AutomationNode} node |
473 * @param {string|!Object} format The output format either specified as an | 436 * @param {string|!Object} format The output format either specified as an |
474 * output template string or a parsed output format tree. | 437 * output template string or a parsed output format tree. |
475 * @param {!Array<cvox.Spannable>} buff Buffer to receive rendered output. | 438 * @param {!cvox.Spannable} buff Buffer to receive rendered output. |
476 * @param {!Object=} opt_exclude A set of attributes to exclude. | 439 * @param {!Object=} opt_exclude A set of attributes to exclude. |
477 * @private | 440 * @private |
478 */ | 441 */ |
479 format_: function(node, format, buff, opt_exclude) { | 442 format_: function(node, format, buff, opt_exclude) { |
480 opt_exclude = opt_exclude || {}; | 443 opt_exclude = opt_exclude || {}; |
481 var tokens = []; | 444 var tokens = []; |
482 var args = null; | 445 var args = null; |
483 | 446 |
484 // Hacky way to support args. | 447 // Hacky way to support args. |
485 if (typeof(format) == 'string') { | 448 if (typeof(format) == 'string') { |
(...skipping 13 matching lines...) Expand all Loading... |
499 if (typeof(token) == 'string') | 462 if (typeof(token) == 'string') |
500 tree = this.createParseTree(token); | 463 tree = this.createParseTree(token); |
501 else | 464 else |
502 tree = token; | 465 tree = token; |
503 | 466 |
504 // Obtain the operator token. | 467 // Obtain the operator token. |
505 token = tree.value; | 468 token = tree.value; |
506 | 469 |
507 // Set suffix options. | 470 // Set suffix options. |
508 var options = {}; | 471 var options = {}; |
509 options.annotation = []; | 472 options.ifEmpty = token[token.length - 1] == '='; |
510 options.isUnique = token[token.length - 1] == '='; | 473 if (options.ifEmpty) |
511 if (options.isUnique) | |
512 token = token.substring(0, token.length - 1); | 474 token = token.substring(0, token.length - 1); |
513 | 475 |
514 // Process token based on prefix. | 476 // Process token based on prefix. |
515 var prefix = token[0]; | 477 var prefix = token[0]; |
516 token = token.slice(1); | 478 token = token.slice(1); |
517 | 479 |
518 if (opt_exclude[token]) | 480 if (opt_exclude[token]) |
519 return; | 481 return; |
520 | 482 |
521 // All possible tokens based on prefix. | 483 // All possible tokens based on prefix. |
522 if (prefix == '$') { | 484 if (prefix == '$') { |
| 485 options.annotation = token; |
523 if (token == 'value') { | 486 if (token == 'value') { |
524 var text = node.attributes.value; | 487 var text = node.attributes.value; |
525 if (text !== undefined) { | 488 if (text !== undefined) { |
| 489 var offset = buff.getLength(); |
526 if (node.attributes.textSelStart !== undefined) { | 490 if (node.attributes.textSelStart !== undefined) { |
527 options.annotation.push(new Output.SelectionSpan( | 491 options.annotation = new Output.SelectionSpan( |
528 node.attributes.textSelStart, | 492 node.attributes.textSelStart, |
529 node.attributes.textSelEnd)); | 493 node.attributes.textSelEnd); |
530 } | 494 } |
| 495 } else if (node.role == chrome.automation.RoleType.staticText) { |
| 496 // TODO(dtseng): Remove once Blink treats staticText values as |
| 497 // names. |
| 498 text = node.attributes.name; |
531 } | 499 } |
532 // Annotate this as a name so we don't duplicate names from ancestors. | 500 this.addToSpannable_(buff, text, options); |
533 if (node.role == chrome.automation.RoleType.inlineTextBox) | |
534 token = 'name'; | |
535 options.annotation.push(token); | |
536 this.append_(buff, text, options); | |
537 } else if (token == 'indexInParent') { | 501 } else if (token == 'indexInParent') { |
538 options.annotation.push(token); | 502 this.addToSpannable_(buff, node.indexInParent + 1); |
539 this.append_(buff, node.indexInParent + 1); | |
540 } else if (token == 'parentChildCount') { | 503 } else if (token == 'parentChildCount') { |
541 options.annotation.push(token); | |
542 if (node.parent) | 504 if (node.parent) |
543 this.append_(buff, node.parent.children.length); | 505 this.addToSpannable_(buff, node.parent.children.length); |
544 } else if (token == 'state') { | 506 } else if (token == 'state') { |
545 options.annotation.push(token); | |
546 Object.getOwnPropertyNames(node.state).forEach(function(s) { | 507 Object.getOwnPropertyNames(node.state).forEach(function(s) { |
547 this.append_(buff, s, options); | 508 this.addToSpannable_(buff, s, options); |
548 }.bind(this)); | 509 }.bind(this)); |
549 } else if (token == 'find') { | 510 } else if (token == 'find') { |
550 // Find takes two arguments: JSON query string and format string. | 511 // Find takes two arguments: JSON query string and format string. |
551 if (tree.firstChild) { | 512 if (tree.firstChild) { |
552 var jsonQuery = tree.firstChild.value; | 513 var jsonQuery = tree.firstChild.value; |
553 node = node.find( | 514 node = node.find( |
554 /** @type {Object}*/(JSON.parse(jsonQuery))); | 515 /** @type {Object}*/(JSON.parse(jsonQuery))); |
555 var formatString = tree.firstChild.nextSibling; | 516 var formatString = tree.firstChild.nextSibling; |
556 if (node) | 517 if (node) |
557 this.format_(node, formatString, buff); | 518 this.format_(node, formatString, buff); |
558 } | 519 } |
559 } else if (token == 'descendants') { | 520 } else if (token == 'descendants') { |
560 if (AutomationPredicate.leaf(node)) | 521 if (AutomationPredicate.leaf(node)) |
561 return; | 522 return; |
562 | 523 |
563 // Construct a range to the leftmost and rightmost leaves. | 524 // Construct a range to the leftmost and rightmost leaves. |
564 var leftmost = AutomationUtil.findNodePre( | 525 var leftmost = AutomationUtil.findNodePre( |
565 node, Dir.FORWARD, AutomationPredicate.leaf); | 526 node, Dir.FORWARD, AutomationPredicate.leaf); |
566 var rightmost = AutomationUtil.findNodePre( | 527 var rightmost = AutomationUtil.findNodePre( |
567 node, Dir.BACKWARD, AutomationPredicate.leaf); | 528 node, Dir.BACKWARD, AutomationPredicate.leaf); |
568 if (!leftmost || !rightmost) | 529 if (!leftmost || !rightmost) |
569 return; | 530 return; |
570 | 531 |
571 var subrange = new cursors.Range( | 532 var subrange = new cursors.Range( |
572 new cursors.Cursor(leftmost, 0), | 533 new cursors.Cursor(leftmost, 0), |
573 new cursors.Cursor(rightmost, 0)); | 534 new cursors.Cursor(rightmost, 0)); |
574 this.range_(subrange, null, 'navigate', buff); | 535 this.range_(subrange, null, 'navigate', buff); |
575 } else if (token == 'role') { | 536 } else if (token == 'role') { |
576 options.annotation.push(token); | |
577 var msg = node.role; | 537 var msg = node.role; |
578 var earconId = null; | 538 var earconId = null; |
579 var info = Output.ROLE_INFO_[node.role]; | 539 var info = Output.ROLE_INFO_[node.role]; |
580 if (info) { | 540 if (info) { |
581 if (this.formatOptions_.braille) | 541 if (this.formatOptions_.braille) |
582 msg = cvox.ChromeVox.msgs.getMsg(info.msgId + '_brl'); | 542 msg = cvox.ChromeVox.msgs.getMsg(info.msgId + '_brl'); |
583 else | 543 else |
584 msg = cvox.ChromeVox.msgs.getMsg(info.msgId); | 544 msg = cvox.ChromeVox.msgs.getMsg(info.msgId); |
585 earconId = info.earcon; | 545 earconId = info.earcon; |
586 } else { | 546 } else { |
587 console.error('Missing role info for ' + node.role); | 547 console.error('Missing role info for ' + node.role); |
588 } | 548 } |
589 if (earconId) | 549 if (earconId) { |
590 options.annotation.push(new Output.EarconAction(earconId)); | 550 options.annotation = new Output.Action(function() { |
591 this.append_(buff, msg, options); | 551 cvox.ChromeVox.earcons.playEarcon( |
592 } else if (node.attributes[token] !== undefined) { | 552 cvox.AbstractEarcons[earconId]); |
593 options.annotation.push(token); | 553 }); |
594 this.append_(buff, node.attributes[token], options); | 554 } |
595 } else if (Output.STATE_INFO_[token]) { | 555 this.addToSpannable_(buff, msg, options); |
596 options.annotation.push('state'); | 556 } else if (node.attributes[token]) { |
597 var stateInfo = Output.STATE_INFO_[token]; | 557 this.addToSpannable_(buff, node.attributes[token], options); |
598 var resolvedInfo = node.state[token] ? stateInfo.on : stateInfo.off; | 558 } else if (node.state[token]) { |
599 options.annotation.push( | 559 this.addToSpannable_(buff, token, options); |
600 new Output.EarconAction(resolvedInfo.earconId)); | |
601 var msgId = | |
602 this.formatOptions_.braille ? resolvedInfo.msgId + '_brl' : | |
603 resolvedInfo.msgId; | |
604 var msg = cvox.ChromeVox.msgs.getMsg(msgId); | |
605 this.append_(buff, msg, options); | |
606 } else if (tree.firstChild) { | 560 } else if (tree.firstChild) { |
607 // Custom functions. | 561 // Custom functions. |
608 if (token == 'if') { | 562 if (token == 'if') { |
609 var cond = tree.firstChild; | 563 var cond = tree.firstChild; |
610 var attrib = cond.value.slice(1); | 564 var attrib = cond.value.slice(1); |
611 if (node.attributes[attrib] || node.state[attrib]) | 565 if (node.attributes[attrib] || node.state[attrib]) |
612 this.format_(node, cond.nextSibling, buff); | 566 this.format_(node, cond.nextSibling, buff); |
613 else | 567 else |
614 this.format_(node, cond.nextSibling.nextSibling, buff); | 568 this.format_(node, cond.nextSibling.nextSibling, buff); |
615 } else if (token == 'earcon') { | 569 } else if (token == 'earcon') { |
616 // Assumes there's existing output in our buffer. | 570 var contentBuff = new cvox.Spannable(); |
617 var lastBuff = buff[buff.length - 1]; | 571 if (tree.firstChild.nextSibling) |
618 if (!lastBuff) | 572 this.format_(node, tree.firstChild.nextSibling, contentBuff); |
619 return; | 573 options.annotation = new Output.Action(function() { |
620 | 574 cvox.ChromeVox.earcons.playEarcon( |
621 lastBuff.setSpan( | 575 cvox.AbstractEarcons[tree.firstChild.value]); |
622 new Output.EarconAction(tree.firstChild.value), 0, 0); | 576 }); |
| 577 this.addToSpannable_(buff, contentBuff, options); |
623 } | 578 } |
624 } | 579 } |
625 } else if (prefix == '@') { | 580 } else if (prefix == '@') { |
626 var msgId = token; | 581 var msgId = token; |
627 var msgArgs = []; | 582 var msgArgs = []; |
628 var curMsg = tree.firstChild; | 583 var curMsg = tree.firstChild; |
629 | 584 |
630 while (curMsg) { | 585 while (curMsg) { |
631 var arg = curMsg.value; | 586 var arg = curMsg.value; |
632 if (arg[0] != '$') { | 587 if (arg[0] != '$') { |
633 console.error('Unexpected value: ' + arg); | 588 console.error('Unexpected value: ' + arg); |
634 return; | 589 return; |
635 } | 590 } |
636 var msgBuff = []; | 591 var msgBuff = new cvox.Spannable(); |
637 this.format_(node, arg, msgBuff); | 592 this.format_(node, arg, msgBuff); |
638 msgArgs = msgArgs.concat(msgBuff); | 593 msgArgs.push(msgBuff.toString()); |
639 curMsg = curMsg.nextSibling; | 594 curMsg = curMsg.nextSibling; |
640 } | 595 } |
641 var msg = cvox.ChromeVox.msgs.getMsg(msgId, msgArgs); | 596 var msg = cvox.ChromeVox.msgs.getMsg(msgId, msgArgs); |
642 try { | 597 try { |
643 if (this.formatOptions_.braille) | 598 if (this.formatOptions_.braille) |
644 msg = cvox.ChromeVox.msgs.getMsg(msgId + '_brl', msgArgs) || msg; | 599 msg = cvox.ChromeVox.msgs.getMsg(msgId + '_brl', msgArgs) || msg; |
645 } catch(e) {} | 600 } catch(e) {} |
646 | 601 |
647 if (msg) { | 602 if (msg) { |
648 this.append_(buff, msg, options); | 603 this.addToSpannable_(buff, msg, options); |
649 } | 604 } |
650 } else if (prefix == '!') { | 605 } else if (prefix == '!') { |
651 this.speechProperties_[token] = true; | 606 this.speechProperties_[token] = true; |
652 } | 607 } |
653 }.bind(this)); | 608 }.bind(this)); |
654 }, | 609 }, |
655 | 610 |
656 /** | 611 /** |
657 * @param {!cursors.Range} range | 612 * @param {!cursors.Range} range |
658 * @param {cursors.Range} prevRange | 613 * @param {cursors.Range} prevRange |
659 * @param {chrome.automation.EventType|string} type | 614 * @param {chrome.automation.EventType|string} type |
660 * @param {!Array<cvox.Spannable>} rangeBuff | 615 * @param {!cvox.Spannable} rangeBuff |
661 * @private | 616 * @private |
662 */ | 617 */ |
663 range_: function(range, prevRange, type, rangeBuff) { | 618 range_: function(range, prevRange, type, rangeBuff) { |
664 if (!prevRange) | 619 if (!prevRange) |
665 prevRange = cursors.Range.fromNode(range.getStart().getNode().root); | 620 prevRange = cursors.Range.fromNode(range.getStart().getNode().root); |
666 | 621 |
667 var cursor = range.getStart(); | 622 var cursor = range.getStart(); |
668 var prevNode = prevRange.getStart().getNode(); | 623 var prevNode = prevRange.getStart().getNode(); |
669 | 624 |
670 var formatNodeAndAncestors = function(node, prevNode) { | 625 var formatNodeAndAncestors = function(node, prevNode) { |
671 var buff = []; | 626 var buff = new cvox.Spannable(); |
672 this.ancestry_(node, prevNode, type, buff); | 627 this.ancestry_(node, prevNode, type, buff); |
673 this.node_(node, prevNode, type, buff); | 628 this.node_(node, prevNode, type, buff); |
674 if (this.formatOptions_.location) | 629 if (this.formatOptions_.location) |
675 this.locations_.push(node.location); | 630 this.locations_.push(node.location); |
676 return buff; | 631 return buff; |
677 }.bind(this); | 632 }.bind(this); |
678 | 633 |
679 while (cursor.getNode() != range.getEnd().getNode()) { | 634 while (cursor.getNode() != range.getEnd().getNode()) { |
680 var node = cursor.getNode(); | 635 var node = cursor.getNode(); |
681 rangeBuff.push.apply(rangeBuff, formatNodeAndAncestors(node, prevNode)); | 636 this.addToSpannable_( |
| 637 rangeBuff, formatNodeAndAncestors(node, prevNode)); |
682 prevNode = node; | 638 prevNode = node; |
683 cursor = cursor.move(cursors.Unit.NODE, | 639 cursor = cursor.move(cursors.Unit.NODE, |
684 cursors.Movement.DIRECTIONAL, | 640 cursors.Movement.DIRECTIONAL, |
685 Dir.FORWARD); | 641 Dir.FORWARD); |
686 } | 642 } |
687 var lastNode = range.getEnd().getNode(); | 643 var lastNode = range.getEnd().getNode(); |
688 rangeBuff.push.apply(rangeBuff, formatNodeAndAncestors(lastNode, prevNode)); | 644 this.addToSpannable_(rangeBuff, formatNodeAndAncestors(lastNode, prevNode)); |
689 }, | 645 }, |
690 | 646 |
691 /** | 647 /** |
692 * @param {!chrome.automation.AutomationNode} node | 648 * @param {!chrome.automation.AutomationNode} node |
693 * @param {!chrome.automation.AutomationNode} prevNode | 649 * @param {!chrome.automation.AutomationNode} prevNode |
694 * @param {chrome.automation.EventType|string} type | 650 * @param {chrome.automation.EventType|string} type |
695 * @param {!Array<cvox.Spannable>} buff | 651 * @param {!cvox.Spannable} buff |
696 * @param {!Object=} opt_exclude A list of attributes to exclude from | 652 * @param {!Object=} opt_exclude A list of attributes to exclude from |
697 * processing. | 653 * processing. |
698 * @private | 654 * @private |
699 */ | 655 */ |
700 ancestry_: function(node, prevNode, type, buff, opt_exclude) { | 656 ancestry_: function(node, prevNode, type, buff, opt_exclude) { |
701 opt_exclude = opt_exclude || {}; | 657 opt_exclude = opt_exclude || {}; |
702 var prevUniqueAncestors = | 658 var prevUniqueAncestors = |
703 AutomationUtil.getUniqueAncestors(node, prevNode); | 659 AutomationUtil.getUniqueAncestors(node, prevNode); |
704 var uniqueAncestors = AutomationUtil.getUniqueAncestors(prevNode, node); | 660 var uniqueAncestors = AutomationUtil.getUniqueAncestors(prevNode, node); |
705 | 661 |
706 // First, look up the event type's format block. | 662 // First, look up the event type's format block. |
707 // Navigate is the default event. | 663 // Navigate is the default event. |
708 var eventBlock = Output.RULES[type] || Output.RULES['navigate']; | 664 var eventBlock = Output.RULES[type] || Output.RULES['navigate']; |
709 | 665 |
710 for (var i = 0, formatPrevNode; | 666 for (var i = 0, formatPrevNode; |
711 (formatPrevNode = prevUniqueAncestors[i]); | 667 (formatPrevNode = prevUniqueAncestors[i]); |
712 i++) { | 668 i++) { |
713 var roleBlock = eventBlock[formatPrevNode.role] || eventBlock['default']; | 669 var roleBlock = eventBlock[formatPrevNode.role] || eventBlock['default']; |
714 if (roleBlock.leave) | 670 if (roleBlock.leave) |
715 this.format_(formatPrevNode, roleBlock.leave, buff, opt_exclude); | 671 this.format_(formatPrevNode, roleBlock.leave, buff, opt_exclude); |
716 } | 672 } |
717 | 673 |
718 var enterOutputs = []; | 674 var enterOutput = []; |
719 var enterRole = {}; | 675 var enterRole = {}; |
720 for (var j = uniqueAncestors.length - 2, formatNode; | 676 for (var j = uniqueAncestors.length - 2, formatNode; |
721 (formatNode = uniqueAncestors[j]); | 677 (formatNode = uniqueAncestors[j]); |
722 j--) { | 678 j--) { |
723 var roleBlock = eventBlock[formatNode.role] || eventBlock['default']; | 679 var roleBlock = eventBlock[formatNode.role] || eventBlock['default']; |
724 if (roleBlock.enter) { | 680 if (roleBlock.enter) { |
725 if (enterRole[formatNode.role]) | 681 if (enterRole[formatNode.role]) |
726 continue; | 682 continue; |
727 enterRole[formatNode.role] = true; | 683 enterRole[formatNode.role] = true; |
728 var tempBuff = []; | 684 var tempBuff = new cvox.Spannable(''); |
729 this.format_(formatNode, roleBlock.enter, tempBuff, opt_exclude); | 685 this.format_(formatNode, roleBlock.enter, tempBuff, opt_exclude); |
730 enterOutputs.unshift(tempBuff); | 686 enterOutput.unshift(tempBuff); |
731 } | 687 } |
732 if (formatNode.role == 'window') | |
733 break; | |
734 } | 688 } |
735 enterOutputs.forEach(function(b) { | 689 enterOutput.forEach(function(c) { |
736 buff.push.apply(buff, b); | 690 this.addToSpannable_(buff, c); |
737 }); | 691 }.bind(this)); |
738 | 692 |
739 if (!opt_exclude.stay) { | 693 if (!opt_exclude.stay) { |
740 var commonFormatNode = uniqueAncestors[0]; | 694 var commonFormatNode = uniqueAncestors[0]; |
741 while (commonFormatNode && commonFormatNode.parent) { | 695 while (commonFormatNode && commonFormatNode.parent) { |
742 commonFormatNode = commonFormatNode.parent; | 696 commonFormatNode = commonFormatNode.parent; |
743 var roleBlock = | 697 var roleBlock = |
744 eventBlock[commonFormatNode.role] || eventBlock['default']; | 698 eventBlock[commonFormatNode.role] || eventBlock['default']; |
745 if (roleBlock.stay) | 699 if (roleBlock.stay) |
746 this.format_(commonFormatNode, roleBlock.stay, buff, opt_exclude); | 700 this.format_(commonFormatNode, roleBlock.stay, buff, opt_exclude); |
747 } | 701 } |
748 } | 702 } |
749 }, | 703 }, |
750 | 704 |
751 /** | 705 /** |
752 * @param {!chrome.automation.AutomationNode} node | 706 * @param {!chrome.automation.AutomationNode} node |
753 * @param {!chrome.automation.AutomationNode} prevNode | 707 * @param {!chrome.automation.AutomationNode} prevNode |
754 * @param {chrome.automation.EventType|string} type | 708 * @param {chrome.automation.EventType|string} type |
755 * @param {!Array<cvox.Spannable>} buff | 709 * @param {!cvox.Spannable} buff |
756 * @private | 710 * @private |
757 */ | 711 */ |
758 node_: function(node, prevNode, type, buff) { | 712 node_: function(node, prevNode, type, buff) { |
759 // Navigate is the default event. | 713 // Navigate is the default event. |
760 var eventBlock = Output.RULES[type] || Output.RULES['navigate']; | 714 var eventBlock = Output.RULES[type] || Output.RULES['navigate']; |
761 var roleBlock = eventBlock[node.role] || eventBlock['default']; | 715 var roleBlock = eventBlock[node.role] || eventBlock['default']; |
762 var speakFormat = roleBlock.speak || eventBlock['default'].speak; | 716 var speakFormat = roleBlock.speak || eventBlock['default'].speak; |
763 this.format_(node, speakFormat, buff); | 717 this.format_(node, speakFormat, buff); |
764 }, | 718 }, |
765 | 719 |
766 /** | 720 /** |
767 * @param {!cursors.Range} range | 721 * @param {!cursors.Range} range |
768 * @param {cursors.Range} prevRange | 722 * @param {cursors.Range} prevRange |
769 * @param {chrome.automation.EventType|string} type | 723 * @param {chrome.automation.EventType|string} type |
770 * @param {!Array<cvox.Spannable>} buff | 724 * @param {!cvox.Spannable} buff |
771 * @private | 725 * @private |
772 */ | 726 */ |
773 subNode_: function(range, prevRange, type, buff) { | 727 subNode_: function(range, prevRange, type, buff) { |
774 if (!prevRange) | 728 if (!prevRange) |
775 prevRange = range; | 729 prevRange = range; |
776 var dir = cursors.Range.getDirection(prevRange, range); | 730 var dir = cursors.Range.getDirection(prevRange, range); |
777 var prevNode = prevRange.getBound(dir).getNode(); | 731 var prevNode = prevRange.getBound(dir).getNode(); |
778 this.ancestry_( | 732 this.ancestry_( |
779 range.getStart().getNode(), prevNode, type, buff, | 733 range.getStart().getNode(), prevNode, type, buff, |
780 {stay: true, name: true, value: true}); | 734 {stay: true, name: true, value: true}); |
781 var startIndex = range.getStart().getIndex(); | 735 var startIndex = range.getStart().getIndex(); |
782 var endIndex = range.getEnd().getIndex(); | 736 var endIndex = range.getEnd().getIndex(); |
783 if (startIndex === endIndex) | 737 if (startIndex === endIndex) |
784 endIndex++; | 738 endIndex++; |
785 this.append_( | 739 this.addToSpannable_( |
786 buff, range.getStart().getText().substring(startIndex, endIndex)); | 740 buff, range.getStart().getText().substring(startIndex, endIndex)); |
787 }, | 741 }, |
788 | 742 |
789 /** | 743 /** |
790 * Appends output to the |buff|. | 744 * Adds to the given buffer with proper delimiters added. |
791 * @param {!Array<cvox.Spannable>} buff | 745 * @param {!cvox.Spannable} spannable |
792 * @param {string|!cvox.Spannable} value | 746 * @param {string|!cvox.Spannable} value |
793 * @param {{isUnique: (boolean|undefined), | 747 * @param {{ifEmpty: boolean, |
794 * annotation: !Array<*>}=} opt_options | 748 * annotation: (string|Output.Action|undefined)}=} opt_options |
795 */ | 749 */ |
796 append_: function(buff, value, opt_options) { | 750 addToSpannable_: function(spannable, value, opt_options) { |
797 opt_options = opt_options || {isUnique: false, annotation: []}; | 751 opt_options = opt_options || {ifEmpty: false, annotation: undefined}; |
798 | 752 if ((!value || value.length == 0) && !opt_options.annotation) |
799 // Reject empty values without annotations. | |
800 if ((!value || value.length == 0) && opt_options.annotation.length == 0) | |
801 return; | 753 return; |
802 | 754 |
803 var spannableToAdd = new cvox.Spannable(value); | 755 var spannableToAdd = new cvox.Spannable(value, opt_options.annotation); |
804 opt_options.annotation.forEach(function(a) { | 756 if (spannable.getLength() == 0) { |
805 spannableToAdd.setSpan(a, 0, spannableToAdd.getLength()); | 757 spannable.append(spannableToAdd); |
806 }); | |
807 | |
808 // Early return if the buffer is empty. | |
809 if (buff.length == 0) { | |
810 buff.push(spannableToAdd); | |
811 return; | 758 return; |
812 } | 759 } |
813 | 760 |
814 // |isUnique| specifies an annotation that cannot be duplicated. | 761 if (opt_options.ifEmpty && |
815 if (opt_options.isUnique) { | 762 opt_options.annotation && |
816 var alreadyAnnotated = buff.some(function(s) { | 763 (spannable.getSpanStart(opt_options.annotation) != undefined || |
817 return opt_options.annotation.some(function(annotation) { | 764 spannable.getSpanStart( |
818 return s.getSpanStart(annotation) != undefined; | 765 Output.ATTRIBUTE_ALIAS[opt_options.annotation]) != undefined)) |
819 }); | 766 return; |
820 }); | |
821 if (alreadyAnnotated) | |
822 return; | |
823 } | |
824 | 767 |
825 buff.push(spannableToAdd); | 768 var prefixed = new cvox.Spannable(Output.SPACE); |
| 769 prefixed.append(spannableToAdd); |
| 770 spannable.append(prefixed); |
826 }, | 771 }, |
827 | 772 |
828 /** | 773 /** |
829 * Parses the token containing a custom function and returns a tree. | 774 * Parses the token containing a custom function and returns a tree. |
830 * @param {string} inputStr | 775 * @param {string} inputStr |
831 * @return {Object} | 776 * @return {Object} |
832 */ | 777 */ |
833 createParseTree: function(inputStr) { | 778 createParseTree: function(inputStr) { |
834 var root = {value: ''}; | 779 var root = {value: ''}; |
835 var currentNode = root; | 780 var currentNode = root; |
(...skipping 23 matching lines...) Expand all Loading... |
859 } | 804 } |
860 | 805 |
861 if (currentNode != root) | 806 if (currentNode != root) |
862 throw 'Unbalanced parenthesis.'; | 807 throw 'Unbalanced parenthesis.'; |
863 | 808 |
864 return root; | 809 return root; |
865 } | 810 } |
866 }; | 811 }; |
867 | 812 |
868 }); // goog.scope | 813 }); // goog.scope |
OLD | NEW |