Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(328)

Side by Side Diff: chrome/browser/resources/chromeos/chromevox/cvox2/background/output.js

Issue 1035983003: Split spoken feedback up on a per format rule basis. (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: Address comments. Created 5 years, 8 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
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
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
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
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
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
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
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
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698