Chromium Code Reviews| 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. | |
|
dmazzoni
2015/04/13 22:56:52
To clarify, this will eventually include other sta
David Tseng
2015/04/15 17:58:35
Yes; that's right.
| |
| 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) { |
| 369 if (this.speechStartCallback_) | 395 if (buff.toString()) { |
| 370 this.speechProperties_['startCallback'] = this.speechStartCallback_; | 396 if (this.speechStartCallback_) |
| 371 if (this.speechEndCallback_) { | 397 this.speechProperties_['startCallback'] = this.speechStartCallback_; |
|
dmazzoni
2015/04/13 22:56:52
It seems to me like this will execute the start an
David Tseng
2015/04/15 18:00:51
Done.
| |
| 372 this.speechProperties_['endCallback'] = this.speechEndCallback_; | 398 else |
| 399 this.speechProperties_['startCallback'] = null; | |
| 400 if (this.speechEndCallback_) | |
| 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 |