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