Only grant permissions to new extensions from sync if they have the expected version
[chromium-blink-merge.git] / chrome / browser / resources / chromeos / chromevox / cvox2 / background / output.js
blob639ca1ee43cdae38519d4dc259d632cfdc421ae6
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
3 // found in the LICENSE file.
5 /**
6  * @fileoverview Provides output services for ChromeVox.
7  */
9 goog.provide('Output');
10 goog.provide('Output.EventType');
12 goog.require('AutomationUtil.Dir');
13 goog.require('cursors.Cursor');
14 goog.require('cursors.Range');
15 goog.require('cursors.Unit');
16 goog.require('cvox.AbstractEarcons');
17 goog.require('cvox.NavBraille');
18 goog.require('cvox.Spannable');
19 goog.require('cvox.ValueSelectionSpan');
20 goog.require('cvox.ValueSpan');
21 goog.require('goog.i18n.MessageFormat');
23 goog.scope(function() {
24 var Dir = AutomationUtil.Dir;
26 /**
27  * An Output object formats a cursors.Range into speech, braille, or both
28  * representations. This is typically a cvox.Spannable.
29  *
30  * The translation from Range to these output representations rely upon format
31  * rules which specify how to convert AutomationNode objects into annotated
32  * strings.
33  * The format of these rules is as follows.
34  *
35  * $ prefix: used to substitute either an attribute or a specialized value from
36  *     an AutomationNode. Specialized values include role and state.
37  *     For example, $value $role $enabled
38  * @ prefix: used to substitute a message. Note the ability to specify params to
39  *     the message.  For example, '@tag_html' '@selected_index($text_sel_start,
40  *     $text_sel_end').
41  * @@ prefix: similar to @, used to substitute a message, but also pulls the
42  *     localized string through goog.i18n.MessageFormat to support locale
43  *     aware plural handling.  The first argument should be a number which will
44  *     be passed as a COUNT named parameter to MessageFormat.
45  *     TODO(plundblad): Make subsequent arguments normal placeholder arguments
46  *     when needed.
47  * = suffix: used to specify substitution only if not previously appended.
48  *     For example, $name= would insert the name attribute only if no name
49  * attribute had been inserted previously.
50  * @constructor
51  */
52 Output = function() {
53   // TODO(dtseng): Include braille specific rules.
54   /** @type {!Array<!cvox.Spannable>} */
55   this.speechBuffer_ = [];
56   /** @type {!Array<!cvox.Spannable>} */
57   this.brailleBuffer_ = [];
58   /** @type {!Array<!Object>} */
59   this.locations_ = [];
60   /** @type {function(?)} */
61   this.speechEndCallback_;
63   /**
64    * Current global options.
65    * @type {{speech: boolean, braille: boolean, location: boolean}}
66    */
67   this.formatOptions_ = {speech: true, braille: false, location: true};
69   /**
70    * Speech properties to apply to the entire output.
71    * @type {!Object<*>}
72    */
73   this.speechProperties_ = {};
76 /**
77  * Delimiter to use between output values.
78  * @type {string}
79  */
80 Output.SPACE = ' ';
82 /**
83  * Metadata about supported automation roles.
84  * @const {Object<{msgId: string,
85  *                 earconId: (string|undefined),
86  *                 inherits: (string|undefined)}>}
87  * msgId: the message id of the role.
88  * earconId: an optional earcon to play when encountering the role.
89  * inherits: inherits rules from this role.
90  * @private
91  */
92 Output.ROLE_INFO_ = {
93   alert: {
94     msgId: 'aria_role_alert',
95     earconId: 'ALERT_NONMODAL',
96   },
97   alertDialog: {
98     msgId: 'aria_role_alertdialog'
99   },
100   article: {
101     msgId: 'aria_role_article',
102     inherits: 'abstractContainer'
103   },
104   application: {
105     msgId: 'aria_role_application',
106     inherits: 'abstractContainer'
107   },
108   banner: {
109     msgId: 'aria_role_banner',
110     inherits: 'abstractContainer'
111   },
112   button: {
113     msgId: 'tag_button',
114     earconId: 'BUTTON'
115   },
116   buttonDropDown: {
117     msgId: 'tag_button',
118     earconId: 'BUTTON'
119   },
120   cell: {
121     msgId: 'aria_role_gridcell'
122   },
123   checkBox: {
124     msgId: 'input_type_checkbox'
125   },
126   columnHeader: {
127     msgId: 'aria_role_columnheader',
128     inherits: 'abstractContainer'
129   },
130   comboBox: {
131     msgId: 'aria_role_combobox'
132   },
133   complementary: {
134     msgId: 'aria_role_complementary',
135     inherits: 'abstractContainer'
136   },
137   contentInfo: {
138     msgId: 'aria_role_contentinfo',
139     inherits: 'abstractContainer'
140   },
141   date: {
142     msgId: 'input_type_date',
143     inherits: 'abstractContainer'
144   },
145   definition: {
146     msgId: 'aria_role_definition',
147     inherits: 'abstractContainer'
148   },
149   dialog: {
150     msgId: 'dialog'
151   },
152   directory: {
153     msgId: 'aria_role_directory',
154     inherits: 'abstractContainer'
155   },
156   document: {
157     msgId: 'aria_role_document',
158     inherits: 'abstractContainer'
159   },
160   form: {
161     msgId: 'aria_role_form',
162     inherits: 'abstractContainer'
163   },
164   grid: {
165     msgId: 'aria_role_grid'
166   },
167   group: {
168     msgId: 'aria_role_group'
169   },
170   heading: {
171     msgId: 'aria_role_heading',
172   },
173   image: {
174     msgId: 'aria_role_img',
175   },
176   inputTime: {
177     msgId: 'input_type_time',
178     inherits: 'abstractContainer'
179   },
180   link: {
181     msgId: 'tag_link',
182     earconId: 'LINK'
183   },
184   listBox: {
185     msgId: 'aria_role_listbox',
186     earconId: 'LISTBOX'
187   },
188   listBoxOption: {
189     msgId: 'aria_role_listitem',
190     earconId: 'LIST_ITEM'
191   },
192   listItem: {
193     msgId: 'aria_role_listitem',
194     earconId: 'LIST_ITEM'
195   },
196   log: {
197     msgId: 'aria_role_log',
198   },
199   main: {
200     msgId: 'aria_role_main',
201     inherits: 'abstractContainer'
202   },
203   marquee: {
204     msgId: 'aria_role_marquee',
205   },
206   math: {
207     msgId: 'aria_role_math',
208     inherits: 'abstractContainer'
209   },
210   menu: {
211     msgId: 'aria_role_menu'
212   },
213   menuBar: {
214     msgId: 'aria_role_menubar',
215   },
216   menuItem: {
217     msgId: 'aria_role_menuitem'
218   },
219   menuItemCheckBox: {
220     msgId: 'aria_role_menuitemcheckbox'
221   },
222   menuItemRadio: {
223     msgId: 'aria_role_menuitemradio'
224   },
225   menuListOption: {
226     msgId: 'aria_role_menuitem'
227   },
228   menuListPopup: {
229     msgId: 'aria_role_menu'
230   },
231   navigation: {
232     msgId: 'aria_role_navigation',
233     inherits: 'abstractContainer'
234   },
235   note: {
236     msgId: 'aria_role_note',
237     inherits: 'abstractContainer'
238   },
239   popUpButton: {
240     msgId: 'tag_button',
241     earcon: 'LISTBOX'
242   },
243   radioButton: {
244     msgId: 'input_type_radio'
245   },
246   radioGroup: {
247     msgId: 'aria_role_radiogroup',
248   },
249   region: {
250     msgId: 'aria_role_region',
251     inherits: 'abstractContainer'
252   },
253   rowHeader: {
254     msgId: 'aria_role_rowheader',
255     inherits: 'abstractContainer'
256   },
257   scrollBar: {
258     msgId: 'aria_role_scrollbar',
259   },
260   search: {
261     msgId: 'aria_role_search',
262     inherits: 'abstractContainer'
263   },
264   separator: {
265     msgId: 'aria_role_separator',
266     inherits: 'abstractContainer'
267   },
268   spinButton: {
269     msgId: 'aria_role_spinbutton',
270     earconId: 'LISTBOX'
271   },
272   status: {
273     msgId: 'aria_role_status'
274   },
275   tab: {
276     msgId: 'aria_role_tab'
277   },
278   tabList: {
279     msgId: 'aria_role_tablist'
280   },
281   tabPanel: {
282     msgId: 'aria_role_tabpanel'
283   },
284   textBox: {
285     msgId: 'input_type_text',
286     earconId: 'EDITABLE_TEXT'
287   },
288   textField: {
289     msgId: 'input_type_text',
290     earconId: 'EDITABLE_TEXT'
291   },
292   time: {
293     msgId: 'tag_time',
294     inherits: 'abstractContainer'
295   },
296   timer: {
297     msgId: 'aria_role_timer'
298   },
299   toolbar: {
300     msgId: 'aria_role_toolbar'
301   },
302   tree: {
303     msgId: 'aria_role_tree'
304   },
305   treeItem: {
306     msgId: 'aria_role_treeitem'
307   }
311  * Metadata about supported automation states.
312  * @const {!Object<{on: {msgId: string, earconId: string},
313  *                  off: {msgId: string, earconId: string},
314  *                  omitted: {msgId: string, earconId: string}}>}
315  *     on: info used to describe a state that is set to true.
316  *     off: info used to describe a state that is set to false.
317  *     omitted: info used to describe a state that is undefined.
318  * @private
319  */
320 Output.STATE_INFO_ = {
321   checked: {
322     on: {
323       earconId: 'CHECK_ON',
324       msgId: 'checkbox_checked_state'
325     },
326     off: {
327       earconId: 'CHECK_OFF',
328       msgId: 'checkbox_unchecked_state'
329     },
330     omitted: {
331       earconId: 'CHECK_OFF',
332       msgId: 'checkbox_unchecked_state'
333     }
334   },
335   collapsed: {
336     on: {
337       msgId: 'aria_expanded_false'
338     },
339     off: {
340       msgId: 'aria_expanded_true'
341     }
342   },
343   expanded: {
344     on: {
345       msgId: 'aria_expanded_true'
346     },
347     off: {
348       msgId: 'aria_expanded_false'
349     }
350   },
351   visited: {
352     on: {
353       msgId: 'visited_state'
354     }
355   }
359  * Rules specifying format of AutomationNodes for output.
360  * @type {!Object<Object<Object<string>>>}
361  */
362 Output.RULES = {
363   navigate: {
364     'default': {
365       speak: '$name $value $help $role',
366       braille: ''
367     },
368     abstractContainer: {
369       enter: '$name $role',
370       leave: '@exited_container($role)'
371     },
372     alert: {
373       speak: '!doNotInterrupt $role $descendants'
374     },
375     alertDialog: {
376       enter: '$name $role $descendants'
377     },
378     cell: {
379       enter: '@column_granularity $tableCellColumnIndex'
380     },
381     checkBox: {
382       speak: '$name $role $checked'
383     },
384     dialog: {
385       enter: '$name $role'
386     },
387     grid: {
388       enter: '$name $role'
389     },
390     heading: {
391       enter: '@tag_h+$hierarchicalLevel',
392       speak: '@tag_h+$hierarchicalLevel $nameOrDescendants='
393     },
394     inlineTextBox: {
395       speak: '$value='
396     },
397     link: {
398       enter: '$name $if($visited, @visited_link, $role)',
399       stay: '$name= $if($visited, @visited_link, $role)',
400       speak: '$name= $if($visited, @visited_link, $role)'
401     },
402     list: {
403       enter: '$role @@list_with_items($countChildren(listItem))'
404     },
405     listBox: {
406       enter: '$name $role @@list_with_items($countChildren(listBoxOption))'
407     },
408     listBoxOption: {
409       speak: '$name $role @describe_index($indexInParent, $parentChildCount)'
410     },
411     listItem: {
412       enter: '$role'
413     },
414     menu: {
415       enter: '$name $role @@list_with_items($countChildren(menuItem))'
416     },
417     menuItem: {
418       speak: '$name $role $if($haspopup, @has_submenu) ' +
419           '@describe_index($indexInParent, $parentChildCount)'
420     },
421     menuListOption: {
422       speak: '$name $value @aria_role_menuitem ' +
423           '@describe_index($indexInParent, $parentChildCount)'
424     },
425     paragraph: {
426       speak: '$descendants'
427     },
428     popUpButton: {
429       speak: '$value $name $role @aria_has_popup ' +
430           '$if($collapsed, @aria_expanded_false, @aria_expanded_true)'
431     },
432     radioButton: {
433       speak: '$if($checked, @describe_radio_selected($name), ' +
434           '@describe_radio_unselected($name))'
435     },
436     radioGroup: {
437       enter: '$name $role'
438     },
439     rootWebArea: {
440       enter: '$name'
441     },
442     row: {
443       enter: '@row_granularity $tableRowIndex'
444     },
445     slider: {
446       speak: '@describe_slider($value, $name) $help'
447     },
448     staticText: {
449       speak: '$value $name'
450     },
451     tab: {
452       speak: '@describe_tab($name)'
453     },
454     textField: {
455       speak: '$name $value $if(' +
456           '$inputType, @input_type_+$inputType, @input_type_text)',
457       braille: ''
458     },
459     toolbar: {
460       enter: '$name $role'
461     },
462     tree: {
463       enter: '$name $role @@list_with_items($countChildren(treeItem))'
464     },
465     treeItem: {
466       enter: '$role $expanded $collapsed ' +
467           '@describe_index($indexInParent, $parentChildCount) ' +
468           '@describe_depth($hierarchicalLevel)'
469     },
470     window: {
471       enter: '$name',
472       speak: '@describe_window($name) $earcon(OBJECT_OPEN)'
473     }
474   },
475   menuStart: {
476     'default': {
477       speak: '@chrome_menu_opened($name)  $earcon(OBJECT_OPEN)'
478     }
479   },
480   menuEnd: {
481     'default': {
482       speak: '@chrome_menu_closed $earcon(OBJECT_CLOSE)'
483     }
484   },
485   menuListValueChanged: {
486     'default': {
487       speak: '$value $name ' +
488           '$find({"state": {"selected": true, "invisible": false}}, ' +
489               '@describe_index($indexInParent, $parentChildCount)) '
490     }
491   },
492   alert: {
493     default: {
494       speak: '!doNotInterrupt ' +
495           '@aria_role_alert $name $earcon(ALERT_NONMODAL) $descendants'
496     }
497   }
501  * Custom actions performed while rendering an output string.
502  * @constructor
503  */
504 Output.Action = function() {
507 Output.Action.prototype = {
508   run: function() {
509   }
513  * Action to play an earcon.
514  * @param {string} earconId
515  * @constructor
516  * @extends {Output.Action}
517  */
518 Output.EarconAction = function(earconId) {
519   Output.Action.call(this);
520   /** @type {string} */
521   this.earconId = earconId;
524 Output.EarconAction.prototype = {
525   __proto__: Output.Action.prototype,
527   /** @override */
528   run: function() {
529     cvox.ChromeVox.earcons.playEarcon(cvox.Earcon[this.earconId]);
530   }
534  * Annotation for selection.
535  * @param {number} startIndex
536  * @param {number} endIndex
537  * @constructor
538  */
539 Output.SelectionSpan = function(startIndex, endIndex) {
540   // TODO(dtseng): Direction lost below; should preserve for braille panning.
541   this.startIndex = startIndex < endIndex ? startIndex : endIndex;
542   this.endIndex = endIndex > startIndex ? endIndex : startIndex;
546  * Wrapper for automation nodes as annotations.  Since the
547  * {@code chrome.automation.AutomationNode} constructor isn't exposed in
548  * the API, this class is used to allow isntanceof checks on these
549  * annotations.
550  @ @param {chrome.automation.AutomationNode} node
551  * @constructor
552  */
553 Output.NodeSpan = function(node) {
554   this.node = node;
558  * Possible events handled by ChromeVox internally.
559  * @enum {string}
560  */
561 Output.EventType = {
562   NAVIGATE: 'navigate'
565 Output.prototype = {
566   /**
567    * Gets the spoken output with separator '|'.
568    * @return {!cvox.Spannable}
569    */
570   get speechOutputForTest() {
571     return this.speechBuffer_.reduce(function(prev, cur) {
572       if (prev === null)
573         return cur;
574       prev.append('|');
575       prev.append(cur);
576       return prev;
577     }, null);
578   },
580   /**
581    * Gets the output buffer for braille.
582    * @return {!cvox.Spannable}
583    */
584   get brailleOutputForTest() {
585     return this.createBrailleOutput_();
586   },
588   /**
589    * Specify ranges for speech.
590    * @param {!cursors.Range} range
591    * @param {cursors.Range} prevRange
592    * @param {chrome.automation.EventType|Output.EventType} type
593    * @return {!Output}
594    */
595   withSpeech: function(range, prevRange, type) {
596     this.formatOptions_ = {speech: true, braille: false, location: true};
597     this.render_(range, prevRange, type, this.speechBuffer_);
598     return this;
599   },
601   /**
602    * Specify ranges for braille.
603    * @param {!cursors.Range} range
604    * @param {cursors.Range} prevRange
605    * @param {chrome.automation.EventType|Output.EventType} type
606    * @return {!Output}
607    */
608   withBraille: function(range, prevRange, type) {
609     this.formatOptions_ = {speech: false, braille: true, location: false};
610     this.render_(range, prevRange, type, this.brailleBuffer_);
611     return this;
612   },
614   /**
615    * Specify the same ranges for speech and braille.
616    * @param {!cursors.Range} range
617    * @param {cursors.Range} prevRange
618    * @param {chrome.automation.EventType|Output.EventType} type
619    * @return {!Output}
620    */
621   withSpeechAndBraille: function(range, prevRange, type) {
622     this.withSpeech(range, prevRange, type);
623     this.withBraille(range, prevRange, type);
624     return this;
625   },
627   /**
628    * Apply a format string directly to the output buffer. This lets you
629    * output a message directly to the buffer using the format syntax.
630    * @param {string} formatStr
631    * @return {!Output}
632    */
633   format: function(formatStr) {
634     this.formatOptions_ = {speech: true, braille: false, location: true};
635     this.format_(null, formatStr, this.speechBuffer_);
637     this.formatOptions_ = {speech: false, braille: true, location: false};
638     this.format_(null, formatStr, this.brailleBuffer_);
640     return this;
641   },
643   /**
644    * Triggers callback for a speech event.
645    * @param {function()} callback
646    */
647   onSpeechEnd: function(callback) {
648     this.speechEndCallback_ = function(opt_cleanupOnly) {
649       if (!opt_cleanupOnly)
650         callback();
651     }.bind(this);
652     return this;
653   },
655   /**
656    * Executes all specified output.
657    */
658   go: function() {
659     // Speech.
660     var queueMode = cvox.QueueMode.FLUSH;
661     this.speechBuffer_.forEach(function(buff, i, a) {
662       if (buff.toString()) {
663         (function() {
664           var scopedBuff = buff;
665           this.speechProperties_['startCallback'] = function() {
666             var actions = scopedBuff.getSpansInstanceOf(Output.Action);
667             if (actions) {
668               actions.forEach(function(a) {
669                 a.run();
670               });
671             }
672           };
673         }.bind(this)());
675         if (this.speechEndCallback_ && i == a.length - 1)
676           this.speechProperties_['endCallback'] = this.speechEndCallback_;
677         else
678           this.speechProperties_['endCallback'] = null;
679         cvox.ChromeVox.tts.speak(
680             buff.toString(), queueMode, this.speechProperties_);
681         queueMode = cvox.QueueMode.QUEUE;
682       }
683     }.bind(this));
685     // Braille.
686     if (this.brailleBuffer_.length) {
687       var buff = this.createBrailleOutput_();
688       var selSpan =
689           buff.getSpanInstanceOf(Output.SelectionSpan);
690       var startIndex = -1, endIndex = -1;
691       if (selSpan) {
692         // Casts ok, since the span is known to be in the spannable.
693         var valueStart =
694             /** @type {number} */ (buff.getSpanStart(selSpan));
695         var valueEnd =
696             /** @type {number} */ (buff.getSpanEnd(selSpan));
697         startIndex = valueStart + selSpan.startIndex;
698         endIndex = valueStart + selSpan.endIndex;
699         buff.setSpan(new cvox.ValueSpan(0), valueStart, valueEnd);
700         buff.setSpan(new cvox.ValueSelectionSpan(), startIndex, endIndex);
701       }
703       var output = new cvox.NavBraille({
704         text: buff,
705         startIndex: startIndex,
706         endIndex: endIndex
707       });
709       cvox.ChromeVox.braille.write(output);
710     }
712     // Display.
713     chrome.accessibilityPrivate.setFocusRing(this.locations_);
714   },
716   /**
717    * Renders the given range using optional context previous range and event
718    * type.
719    * @param {!cursors.Range} range
720    * @param {cursors.Range} prevRange
721    * @param {chrome.automation.EventType|string} type
722    * @param {!Array<cvox.Spannable>} buff Buffer to receive rendered output.
723    * @private
724    */
725   render_: function(range, prevRange, type, buff) {
726     if (range.isSubNode())
727       this.subNode_(range, prevRange, type, buff);
728     else
729       this.range_(range, prevRange, type, buff);
730   },
732   /**
733    * Format the node given the format specifier.
734    * @param {chrome.automation.AutomationNode} node
735    * @param {string|!Object} format The output format either specified as an
736    * output template string or a parsed output format tree.
737    * @param {!Array<cvox.Spannable>} buff Buffer to receive rendered output.
738    * @param {!Object=} opt_exclude A set of attributes to exclude.
739    * @private
740    */
741   format_: function(node, format, buff, opt_exclude) {
742     opt_exclude = opt_exclude || {};
743     var tokens = [];
744     var args = null;
746     // Hacky way to support args.
747     if (typeof(format) == 'string') {
748       format = format.replace(/([,:])\W/g, '$1');
749       tokens = format.split(' ');
750     } else {
751       tokens = [format];
752     }
754     tokens.forEach(function(token) {
755       // Ignore empty tokens.
756       if (!token)
757         return;
759       // Parse the token.
760       var tree;
761       if (typeof(token) == 'string')
762         tree = this.createParseTree_(token);
763       else
764         tree = token;
766       // Obtain the operator token.
767       token = tree.value;
769       // Set suffix options.
770       var options = {};
771       options.annotation = [];
772       options.isUnique = token[token.length - 1] == '=';
773       if (options.isUnique)
774         token = token.substring(0, token.length - 1);
776       // Annotate braille output with the corresponding automation nodes
777       // to support acting on nodes based on location in the output.
778       if (this.formatOptions_.braille)
779         options.annotation.push(new Output.NodeSpan(node));
781       // Process token based on prefix.
782       var prefix = token[0];
783       token = token.slice(1);
785       if (opt_exclude[token])
786         return;
788       // All possible tokens based on prefix.
789       if (prefix == '$') {
790         if (token == 'value') {
791           var text = node.value;
792           if (text !== undefined) {
793             if (node.textSelStart !== undefined) {
794               options.annotation.push(new Output.SelectionSpan(
795                   node.textSelStart,
796                   node.textSelEnd));
797             }
798           }
799           // Annotate this as a name so we don't duplicate names from ancestors.
800           if (node.role == chrome.automation.RoleType.inlineTextBox)
801             token = 'name';
802           options.annotation.push(token);
803           this.append_(buff, text, options);
804         } else if (token == 'name') {
805           options.annotation.push(token);
806           if (this.formatOptions_.speech) {
807             var earconFinder = node;
808             while (earconFinder) {
809               var info = Output.ROLE_INFO_[earconFinder.role];
810               if (info && info.earconId) {
811                 options.annotation.push(
812                     new Output.EarconAction(info.earconId));
813                 break;
814               }
815               earconFinder = earconFinder.parent;
816             }
817           }
819           // Pending finalization of name calculation; we must use the
820           // description property to access aria-label. See crbug.com/473220.
821           var resolvedName = node.description || node.name;
822           this.append_(buff, resolvedName, options);
823         } else if (token == 'nameOrDescendants') {
824           options.annotation.push(token);
825           if (node.name)
826             this.append_(buff, node.name, options);
827           else
828             this.format_(node, '$descendants', buff);
829         } else if (token == 'indexInParent') {
830           options.annotation.push(token);
831           this.append_(buff, String(node.indexInParent + 1));
832         } else if (token == 'parentChildCount') {
833           options.annotation.push(token);
834           if (node.parent)
835             this.append_(buff, String(node.parent.children.length));
836         } else if (token == 'state') {
837           options.annotation.push(token);
838           Object.getOwnPropertyNames(node.state).forEach(function(s) {
839             this.append_(buff, s, options);
840           }.bind(this));
841         } else if (token == 'find') {
842           // Find takes two arguments: JSON query string and format string.
843           if (tree.firstChild) {
844             var jsonQuery = tree.firstChild.value;
845             node = node.find(
846                 /** @type {Object}*/(JSON.parse(jsonQuery)));
847             var formatString = tree.firstChild.nextSibling;
848             if (node)
849               this.format_(node, formatString, buff);
850           }
851         } else if (token == 'descendants') {
852           if (AutomationPredicate.leaf(node))
853             return;
855           // Construct a range to the leftmost and rightmost leaves.
856           var leftmost = AutomationUtil.findNodePre(
857               node, Dir.FORWARD, AutomationPredicate.leaf);
858           var rightmost = AutomationUtil.findNodePre(
859               node, Dir.BACKWARD, AutomationPredicate.leaf);
860           if (!leftmost || !rightmost)
861             return;
863           var subrange = new cursors.Range(
864               new cursors.Cursor(leftmost, 0),
865               new cursors.Cursor(rightmost, 0));
866           var prev = null;
867           if (node)
868             prev = cursors.Range.fromNode(node);
869           this.range_(subrange, prev, 'navigate', buff);
870         } else if (token == 'role') {
871           options.annotation.push(token);
872           var msg = node.role;
873           var info = Output.ROLE_INFO_[node.role];
874           if (info) {
875             if (this.formatOptions_.braille)
876               msg = cvox.ChromeVox.msgs.getMsg(info.msgId + '_brl');
877             else
878               msg = cvox.ChromeVox.msgs.getMsg(info.msgId);
879           } else {
880             console.error('Missing role info for ' + node.role);
881           }
882           this.append_(buff, msg, options);
883         } else if (token == 'tableRowIndex' ||
884             token == 'tableCellColumnIndex') {
885           var value = node[token];
886           if (!value)
887             return;
888           value = String(value + 1);
889           options.annotation.push(token);
890           this.append_(buff, value, options);
891         } else if (node[token] !== undefined) {
892           options.annotation.push(token);
893           var value = node[token];
894           if (typeof value == 'number')
895             value = String(value);
896           this.append_(buff, value, options);
897         } else if (Output.STATE_INFO_[token]) {
898           options.annotation.push('state');
899           var stateInfo = Output.STATE_INFO_[token];
900           var resolvedInfo = {};
901           if (node.state[token] === undefined)
902             resolvedInfo = stateInfo.omitted;
903           else
904             resolvedInfo = node.state[token] ? stateInfo.on : stateInfo.off;
905           if (!resolvedInfo)
906             return;
907           if (this.formatOptions_.speech && resolvedInfo.earconId) {
908             options.annotation.push(
909                 new Output.EarconAction(resolvedInfo.earconId));
910           }
911           var msgId =
912               this.formatOptions_.braille ? resolvedInfo.msgId + '_brl' :
913               resolvedInfo.msgId;
914           var msg = cvox.ChromeVox.msgs.getMsg(msgId);
915           this.append_(buff, msg, options);
916         } else if (tree.firstChild) {
917           // Custom functions.
918           if (token == 'if') {
919             var cond = tree.firstChild;
920             var attrib = cond.value.slice(1);
921             if (node[attrib] || node.state[attrib])
922               this.format_(node, cond.nextSibling, buff);
923             else
924               this.format_(node, cond.nextSibling.nextSibling, buff);
925           } else if (token == 'earcon') {
926             // Ignore unless we're generating speech output.
927             if (!this.formatOptions_.speech)
928               return;
929             // Assumes there's existing output in our buffer.
930             var lastBuff = buff[buff.length - 1];
931             if (!lastBuff)
932               return;
934             lastBuff.setSpan(
935                 new Output.EarconAction(tree.firstChild.value), 0, 0);
936           } else if (token == 'countChildren') {
937             var role = tree.firstChild.value;
938             var count = node.children.filter(function(e) {
939               return e.role == role;
940             }).length;
941             this.append_(buff, String(count));
942           }
943         }
944       } else if (prefix == '@') {
945         var isPluralized = (token[0] == '@');
946         if (isPluralized)
947           token = token.slice(1);
948         // Tokens can have substitutions.
949         var pieces = token.split('+');
950         token = pieces.reduce(function(prev, cur) {
951           var lookup = cur;
952           if (cur[0] == '$')
953             lookup = node[cur.slice(1)];
954           return prev + lookup;
955         }.bind(this), '');
956         var msgId = token;
957         var msgArgs = [];
958         if (!isPluralized) {
959           var curArg = tree.firstChild;
960           while (curArg) {
961             if (curArg.value[0] != '$') {
962               console.error('Unexpected value: ' + curArg.value);
963               return;
964             }
965             var msgBuff = [];
966             this.format_(node, curArg, msgBuff);
967             msgArgs = msgArgs.concat(msgBuff);
968             curArg = curArg.nextSibling;
969           }
970         }
971         var msg = cvox.ChromeVox.msgs.getMsg(msgId, msgArgs);
972         try {
973           if (this.formatOptions_.braille)
974             msg = cvox.ChromeVox.msgs.getMsg(msgId + '_brl', msgArgs) || msg;
975         } catch(e) {}
977         if (!msg) {
978           console.error('Could not get message ' + msgId);
979           return;
980         }
982         if (isPluralized) {
983           var arg = tree.firstChild;
984           if (!arg || arg.nextSibling) {
985             console.error('Pluralized messages take exactly one argument');
986             return;
987           }
988           if (arg.value[0] != '$') {
989             console.error('Unexpected value: ' + arg.value);
990             return;
991           }
992           var argBuff = [];
993           this.format_(node, arg, argBuff);
994           var namedArgs = {COUNT: Number(argBuff[0])};
995           msg = new goog.i18n.MessageFormat(msg).format(namedArgs);
996         }
998         this.append_(buff, msg, options);
999       } else if (prefix == '!') {
1000         this.speechProperties_[token] = true;
1001       }
1002     }.bind(this));
1003   },
1005   /**
1006    * @param {!cursors.Range} range
1007    * @param {cursors.Range} prevRange
1008    * @param {chrome.automation.EventType|string} type
1009    * @param {!Array<cvox.Spannable>} rangeBuff
1010    * @private
1011    */
1012   range_: function(range, prevRange, type, rangeBuff) {
1013     if (!prevRange)
1014       prevRange = cursors.Range.fromNode(range.start.node.root);
1016     var cursor = range.start;
1017     var prevNode = prevRange.start.node;
1019     var formatNodeAndAncestors = function(node, prevNode) {
1020       var buff = [];
1021       this.ancestry_(node, prevNode, type, buff);
1022       this.node_(node, prevNode, type, buff);
1023       if (this.formatOptions_.location)
1024         this.locations_.push(node.location);
1025       return buff;
1026     }.bind(this);
1028     while (cursor.node != range.end.node) {
1029       var node = cursor.node;
1030       rangeBuff.push.apply(rangeBuff, formatNodeAndAncestors(node, prevNode));
1031       prevNode = node;
1032       cursor = cursor.move(cursors.Unit.NODE,
1033                            cursors.Movement.DIRECTIONAL,
1034                            Dir.FORWARD);
1036       // Reached a boundary.
1037       if (cursor.node == prevNode)
1038         break;
1039     }
1040     var lastNode = range.end.node;
1041     rangeBuff.push.apply(rangeBuff, formatNodeAndAncestors(lastNode, prevNode));
1042   },
1044   /**
1045    * @param {!chrome.automation.AutomationNode} node
1046    * @param {!chrome.automation.AutomationNode} prevNode
1047    * @param {chrome.automation.EventType|string} type
1048    * @param {!Array<cvox.Spannable>} buff
1049    * @param {!Object=} opt_exclude A list of attributes to exclude from
1050    * processing.
1051    * @private
1052    */
1053   ancestry_: function(node, prevNode, type, buff, opt_exclude) {
1054     opt_exclude = opt_exclude || {};
1055     var prevUniqueAncestors =
1056         AutomationUtil.getUniqueAncestors(node, prevNode);
1057     var uniqueAncestors = AutomationUtil.getUniqueAncestors(prevNode, node);
1059     // First, look up the event type's format block.
1060     // Navigate is the default event.
1061     var eventBlock = Output.RULES[type] || Output.RULES['navigate'];
1063     var getMergedRoleBlock = function(role) {
1064       var parentRole = (Output.ROLE_INFO_[role] || {}).inherits;
1065       var roleBlock = eventBlock[role] || eventBlock['default'];
1066       var parentRoleBlock = parentRole ? eventBlock[parentRole] : {};
1067       var mergedRoleBlock = {};
1068       for (var key in parentRoleBlock)
1069         mergedRoleBlock[key] = parentRoleBlock[key];
1070       for (var key in roleBlock)
1071         mergedRoleBlock[key] = roleBlock[key];
1072       return mergedRoleBlock;
1073     };
1075     for (var i = 0, formatPrevNode;
1076          (formatPrevNode = prevUniqueAncestors[i]);
1077          i++) {
1078       var roleBlock = getMergedRoleBlock(formatPrevNode.role);
1079       if (roleBlock.leave)
1080         this.format_(formatPrevNode, roleBlock.leave, buff, opt_exclude);
1081     }
1083     var enterOutputs = [];
1084     var enterRole = {};
1085     for (var j = uniqueAncestors.length - 2, formatNode;
1086          (formatNode = uniqueAncestors[j]);
1087          j--) {
1088       var roleBlock = getMergedRoleBlock(formatNode.role);
1089       if (roleBlock.enter) {
1090         if (enterRole[formatNode.role])
1091           continue;
1092         enterRole[formatNode.role] = true;
1093         var tempBuff = [];
1094         this.format_(formatNode, roleBlock.enter, tempBuff, opt_exclude);
1095         enterOutputs.unshift(tempBuff);
1096       }
1097       if (formatNode.role == 'window')
1098         break;
1099     }
1100     enterOutputs.forEach(function(b) {
1101       buff.push.apply(buff, b);
1102     });
1104     if (!opt_exclude.stay) {
1105       var commonFormatNode = uniqueAncestors[0];
1106       while (commonFormatNode && commonFormatNode.parent) {
1107         commonFormatNode = commonFormatNode.parent;
1108         var roleBlock =
1109             eventBlock[commonFormatNode.role] || eventBlock['default'];
1110         if (roleBlock.stay)
1111           this.format_(commonFormatNode, roleBlock.stay, buff, opt_exclude);
1112       }
1113     }
1114   },
1116   /**
1117    * @param {!chrome.automation.AutomationNode} node
1118    * @param {!chrome.automation.AutomationNode} prevNode
1119    * @param {chrome.automation.EventType|string} type
1120    * @param {!Array<cvox.Spannable>} buff
1121    * @private
1122    */
1123   node_: function(node, prevNode, type, buff) {
1124     // Navigate is the default event.
1125     var eventBlock = Output.RULES[type] || Output.RULES['navigate'];
1126     var roleBlock = eventBlock[node.role] || eventBlock['default'];
1127     var speakFormat = roleBlock.speak || eventBlock['default'].speak;
1128     this.format_(node, speakFormat, buff);
1129   },
1131   /**
1132    * @param {!cursors.Range} range
1133    * @param {cursors.Range} prevRange
1134    * @param {chrome.automation.EventType|string} type
1135    * @param {!Array<cvox.Spannable>} buff
1136    * @private
1137    */
1138   subNode_: function(range, prevRange, type, buff) {
1139     if (!prevRange)
1140       prevRange = range;
1141     var dir = cursors.Range.getDirection(prevRange, range);
1142     var prevNode = prevRange.getBound(dir).node;
1143     this.ancestry_(
1144         range.start.node, prevNode, type, buff,
1145         {stay: true, name: true, value: true});
1146     var startIndex = range.start.getIndex();
1147     var endIndex = range.end.getIndex();
1148     if (startIndex === endIndex)
1149       endIndex++;
1150     this.append_(
1151         buff, range.start.getText().substring(startIndex, endIndex));
1152   },
1154   /**
1155    * Appends output to the |buff|.
1156    * @param {!Array<cvox.Spannable>} buff
1157    * @param {string|!cvox.Spannable} value
1158    * @param {{isUnique: (boolean|undefined),
1159    *      annotation: !Array<*>}=} opt_options
1160    */
1161   append_: function(buff, value, opt_options) {
1162     opt_options = opt_options || {isUnique: false, annotation: []};
1164     // Reject empty values without annotations.
1165     if ((!value || value.length == 0) && opt_options.annotation.length == 0)
1166       return;
1168     var spannableToAdd = new cvox.Spannable(value);
1169     opt_options.annotation.forEach(function(a) {
1170       spannableToAdd.setSpan(a, 0, spannableToAdd.getLength());
1171     });
1173     // |isUnique| specifies an annotation that cannot be duplicated.
1174     if (opt_options.isUnique) {
1175       var annotationSansNodes = opt_options.annotation.filter(
1176           function(annotation) {
1177             return !(annotation instanceof Output.NodeSpan);
1178           });
1179       var alreadyAnnotated = buff.some(function(s) {
1180         return annotationSansNodes.some(function(annotation) {
1181           var start = s.getSpanStart(annotation);
1182           var end = s.getSpanEnd(annotation);
1183           if (start === undefined)
1184             return false;
1185           return s.substring(start, end).toString() == value.toString();
1186         });
1187       });
1188       if (alreadyAnnotated)
1189         return;
1190     }
1192     buff.push(spannableToAdd);
1193   },
1195   /**
1196    * Parses the token containing a custom function and returns a tree.
1197    * @param {string} inputStr
1198    * @return {Object}
1199    * @private
1200    */
1201   createParseTree_: function(inputStr) {
1202     var root = {value: ''};
1203     var currentNode = root;
1204     var index = 0;
1205     var braceNesting = 0;
1206     while (index < inputStr.length) {
1207       if (inputStr[index] == '(') {
1208         currentNode.firstChild = {value: ''};
1209         currentNode.firstChild.parent = currentNode;
1210         currentNode = currentNode.firstChild;
1211       } else if (inputStr[index] == ')') {
1212         currentNode = currentNode.parent;
1213       } else if (inputStr[index] == '{') {
1214         braceNesting++;
1215         currentNode.value += inputStr[index];
1216       } else if (inputStr[index] == '}') {
1217         braceNesting--;
1218         currentNode.value += inputStr[index];
1219       } else if (inputStr[index] == ',' && braceNesting === 0) {
1220         currentNode.nextSibling = {value: ''};
1221         currentNode.nextSibling.parent = currentNode.parent;
1222         currentNode = currentNode.nextSibling;
1223       } else {
1224         currentNode.value += inputStr[index];
1225       }
1226       index++;
1227     }
1229     if (currentNode != root)
1230       throw 'Unbalanced parenthesis.';
1232     return root;
1233   },
1235   /**
1236    * Converts the currently rendered braille buffers to a single spannable.
1237    * @return {!cvox.Spannable}
1238    * @private
1239    */
1240   createBrailleOutput_: function() {
1241     var result = new cvox.Spannable();
1242     var separator = '';  // Changes to space as appropriate.
1243     this.brailleBuffer_.forEach(function(cur) {
1244       // If this chunk is empty, don't add it since it won't result
1245       // in any output on the braille display, but node spans would
1246       // start before the separator in that case, which is not desired.
1247       // The exception is if this chunk contains a selectionm, in which
1248       // case it will result in a cursor which has to be preserved.
1249       // In this case, having separators, potentially both before and after
1250       // the empty string is correct.
1251       if (cur.getLength() == 0 && !cur.getSpanInstanceOf(Output.SelectionSpan))
1252         return;
1253       var spansToExtend = [];
1254       var spansToRemove = [];
1255       // Nodes that have node spans both on the character to the left
1256       // of the separator and to the right should also cover the separator.
1257       // We extend the left span to cover both the separator and what the
1258       // right span used to cover, removing the right span, mostly for
1259       // ease of writing tests and debug.
1260       // Note that getSpan(position) never returns zero length spans
1261       // (because they don't cover any position).  Still, we want to include
1262       // these because they can be included (the selection span in an empty
1263       // text field is an example), which is why we write the below code
1264       // using getSpansInstanceOf and check the endpoints (isntead of doing
1265       // the opposite).
1266       result.getSpansInstanceOf(Output.NodeSpan).forEach(function(leftSpan) {
1267         if (result.getSpanEnd(leftSpan) < result.getLength())
1268           return;
1269         var newEnd = result.getLength();
1270         cur.getSpansInstanceOf(Output.NodeSpan).forEach(function(rightSpan) {
1271           if (cur.getSpanStart(rightSpan) == 0 &&
1272               leftSpan.node === rightSpan.node) {
1273             newEnd = Math.max(
1274                 newEnd,
1275                 result.getLength() + separator.length +
1276                     cur.getSpanEnd(rightSpan));
1277             spansToRemove.push(rightSpan);
1278           }
1279         });
1280         if (newEnd > result.getLength())
1281           spansToExtend.push({span: leftSpan, end: newEnd});
1282       });
1283       result.append(separator);
1284       result.append(cur);
1285       spansToExtend.forEach(function(elem) {
1286         result.setSpan(
1287             elem.span,
1288             // Cast ok, since span is known to exist.
1289             /** @type {number} */ (result.getSpanStart(elem.span)),
1290             elem.end);
1291       });
1292       spansToRemove.forEach(result.removeSpan.bind(result));
1293       separator = Output.SPACE;
1294     });
1295     return result;
1296   }
1299 });  // goog.scope