| 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 |