Remove the old signature of NotificationManager::closePersistent().
[chromium-blink-merge.git] / chrome / browser / resources / chromeos / chromevox / cvox2 / background / output.js
blobfd65f4b02db4563eadb8ac6bce258614c390a01f
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');
22 goog.scope(function() {
23 var Dir = AutomationUtil.Dir;
25 /**
26  * An Output object formats a cursors.Range into speech, braille, or both
27  * representations. This is typically a cvox.Spannable.
28  *
29  * The translation from Range to these output representations rely upon format
30  * rules which specify how to convert AutomationNode objects into annotated
31  * strings.
32  * The format of these rules is as follows.
33  *
34  * $ prefix: used to substitute either an attribute or a specialized value from
35  *     an AutomationNode. Specialized values include role and state. Attributes
36  *     available for substitution are AutomationNode.prototype.attributes and
37  *     AutomationNode.prototype.state.
38  *     For example, $value $role $enabled
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,
41  *     $text_sel_end').
42  * = suffix: used to specify substitution only if not previously appended.
43  *     For example, $name= would insert the name attribute only if no name
44  * attribute had been inserted previously.
45  * @constructor
46  */
47 Output = function() {
48   // TODO(dtseng): Include braille specific rules.
49   /** @type {!cvox.Spannable} */
50   this.buffer_ = new cvox.Spannable();
51   /** @type {!cvox.Spannable} */
52   this.brailleBuffer_ = new cvox.Spannable();
53   /** @type {!Array<Object>} */
54   this.locations_ = [];
55   /** @type {function()} */
56   this.speechStartCallback_;
57   /** @type {function()} */
58   this.speechEndCallback_;
60   /**
61    * Current global options.
62    * @type {{speech: boolean, braille: boolean, location: boolean}}
63    */
64   this.formatOptions_ = {speech: true, braille: false, location: true};
66   /**
67    * Speech properties to apply to the entire output.
68    * @type {!Object<string, *>}
69    */
70   this.speechProperties_ = {};
73 /**
74  * Delimiter to use between output values.
75  * @type {string}
76  */
77 Output.SPACE = ' ';
79 /**
80  * Rules specifying format of AutomationNodes for output.
81  * @type {!Object<string, Object<string, Object<string, string>>>}
82  */
83 Output.RULES = {
84   navigate: {
85     'default': {
86       speak: '$name $role $value',
87       braille: ''
88     },
89     alert: {
90       speak: '!doNotInterrupt ' +
91           '@aria_role_alert $name $earcon(ALERT_NONMODAL) $descendants'
92     },
93     button: {
94       speak: '$name $earcon(BUTTON, @tag_button)'
95     },
96     checkBox: {
97       speak: '$if($checked, @describe_checkbox_checked($name), ' +
98           '@describe_checkbox_unchecked($name)) ' +
99           '$if($checked, ' +
100               '$earcon(CHECK_ON, @input_type_checkbox), ' +
101               '$earcon(CHECK_OFF, @input_type_checkbox))'
102     },
103     dialog: {
104       enter: '$name $role'
105     },
106     heading: {
107       enter: '@aria_role_heading',
108       speak: '@aria_role_heading $name='
109     },
110     inlineTextBox: {
111       speak: '$value='
112     },
113     link: {
114       enter: '$name= $visited $earcon(LINK, @tag_link)=',
115       stay: '$name= $visited @tag_link',
116       speak: '$name= $visited $earcon(LINK, @tag_link)='
117     },
118     list: {
119       enter: '$role'
120     },
121     listItem: {
122       enter: '$role'
123     },
124     menuItem: {
125       speak: '$if($haspopup, @describe_menu_item_with_submenu($name), ' +
126           '@describe_menu_item($name)) ' +
127           '@describe_index($indexInParent, $parentChildCount)'
128     },
129     menuListOption: {
130       speak: '$name $value @aria_role_menuitem ' +
131           '@describe_index($indexInParent, $parentChildCount)'
132     },
133     paragraph: {
134       speak: '$value'
135     },
136     popUpButton: {
137       speak: '$value $name @tag_button @aria_has_popup $earcon(LISTBOX) ' +
138           '$if($collapsed, @aria_expanded_false, @aria_expanded_true)'
139     },
140     radioButton: {
141       speak: '$if($checked, @describe_radio_selected($name), ' +
142           '@describe_radio_unselected($name)) ' +
143           '$if($checked, ' +
144               '$earcon(CHECK_ON, @input_type_radio), ' +
145               '$earcon(CHECK_OFF, @input_type_radio))'
146     },
147     slider: {
148       speak: '@describe_slider($value, $name)'
149     },
150     staticText: {
151       speak: '$value'
152     },
153     textBox: {
154       speak: '$name $value $earcon(EDITABLE_TEXT, @input_type_text)'
155     },
156     tab: {
157       speak: '@describe_tab($name)'
158     },
159     textField: {
160       speak: '$name $value $earcon(EDITABLE_TEXT, @input_type_text) $protected'
161     },
162     toolbar: {
163       enter: '$name $role'
164     },
165     window: {
166       enter: '$name',
167       speak: '@describe_window($name) $earcon(OBJECT_OPEN)'
168     }
169   },
170   menuStart: {
171     'default': {
172       speak: '@chrome_menu_opened($name) $role $earcon(OBJECT_OPEN)'
173     }
174   },
175   menuEnd: {
176     'default': {
177       speak: '$earcon(OBJECT_CLOSE)'
178     }
179   },
180   menuListValueChanged: {
181     'default': {
182       speak: '$value $name ' +
183           '$find({"state": {"selected": true, "invisible": false}}, ' +
184               '@describe_index($indexInParent, $parentChildCount)) '
185     }
186   },
187   alert: {
188     default: {
189       speak: '!doNotInterrupt ' +
190           '@aria_role_alert $name $earcon(ALERT_NONMODAL) $descendants'
191     }
192   }
196  * Alias equivalent attributes.
197  * @type {!Object<string, string>}
198  */
199 Output.ATTRIBUTE_ALIAS = {
200   name: 'value',
201   value: 'name'
205  * Custom actions performed while rendering an output string.
206  * @param {function()} action
207  * @constructor
208  */
209 Output.Action = function(action) {
210   this.action_ = action;
213 Output.Action.prototype = {
214   run: function() {
215     this.action_();
216   }
220  * Annotation for selection.
221  * @param {number} startIndex
222  * @param {number} endIndex
223  * @constructor
224  */
225 Output.SelectionSpan = function(startIndex, endIndex) {
226   // TODO(dtseng): Direction lost below; should preserve for braille panning.
227   this.startIndex = startIndex < endIndex ? startIndex : endIndex;
228   this.endIndex = endIndex > startIndex ? endIndex : startIndex;
232  * Possible events handled by ChromeVox internally.
233  * @enum {string}
234  */
235 Output.EventType = {
236   NAVIGATE: 'navigate'
239 Output.prototype = {
240   /**
241    * Gets the output buffer for speech.
242    * @return {!cvox.Spannable}
243    */
244   getBuffer: function() {
245     return this.buffer_;
246   },
248   /**
249    * Specify ranges for speech.
250    * @param {!cursors.Range} range
251    * @param {cursors.Range} prevRange
252    * @param {chrome.automation.EventType|Output.EventType} type
253    * @return {!Output}
254    */
255   withSpeech: function(range, prevRange, type) {
256     this.formatOptions_ = {speech: true, braille: false, location: true};
257     this.render_(range, prevRange, type, this.buffer_);
258     return this;
259   },
261   /**
262    * Specify ranges for braille.
263    * @param {!cursors.Range} range
264    * @param {cursors.Range} prevRange
265    * @param {chrome.automation.EventType|Output.EventType} type
266    * @return {!Output}
267    */
268   withBraille: function(range, prevRange, type) {
269     this.formatOptions_ = {speech: false, braille: true, location: false};
270     this.render_(range, prevRange, type, this.brailleBuffer_);
271     return this;
272   },
274   /**
275    * Specify the same ranges for speech and braille.
276    * @param {!cursors.Range} range
277    * @param {cursors.Range} prevRange
278    * @param {chrome.automation.EventType|Output.EventType} type
279    * @return {!Output}
280    */
281   withSpeechAndBraille: function(range, prevRange, type) {
282     this.withSpeech(range, prevRange, type);
283     this.withBraille(range, prevRange, type);
284     return this;
285   },
287   /**
288    * Triggers callback for a speech event.
289    * @param {function()} callback
290    */
291   onSpeechStart: function(callback) {
292     this.speechStartCallback_ = callback;
293     return this;
294   },
296   /**
297    * Triggers callback for a speech event.
298    * @param {function()} callback
299    */
300   onSpeechEnd: function(callback) {
301     this.speechEndCallback_ = callback;
302     return this;
303   },
305   /**
306    * Executes all specified output.
307    */
308   go: function() {
309     // Speech.
310     var buff = this.buffer_;
311     if (buff.toString()) {
312       if (this.speechStartCallback_)
313         this.speechProperties_['startCallback'] = this.speechStartCallback_;
314       if (this.speechEndCallback_) {
315         this.speechProperties_['endCallback'] = this.speechEndCallback_;
316       }
318       cvox.ChromeVox.tts.speak(
319           buff.toString(), cvox.QueueMode.FLUSH, this.speechProperties_);
320     }
322     var actions = buff.getSpansInstanceOf(Output.Action);
323     if (actions) {
324       actions.forEach(function(a) {
325         a.run();
326       });
327     }
329     // Braille.
330     var selSpan =
331         this.brailleBuffer_.getSpanInstanceOf(Output.SelectionSpan);
332     var startIndex = -1, endIndex = -1;
333     if (selSpan) {
334       var valueStart = this.brailleBuffer_.getSpanStart(selSpan);
335       var valueEnd = this.brailleBuffer_.getSpanEnd(selSpan);
336       if (valueStart === undefined || valueEnd === undefined) {
337         valueStart = -1;
338         valueEnd = -1;
339       } else {
340         startIndex = valueStart + selSpan.startIndex;
341         endIndex = valueStart + selSpan.endIndex;
342         this.brailleBuffer_.setSpan(new cvox.ValueSpan(valueStart),
343                                     valueStart, valueEnd);
344         this.brailleBuffer_.setSpan(new cvox.ValueSelectionSpan(),
345                                     startIndex, endIndex);
346       }
347     }
349     var output = new cvox.NavBraille({
350       text: this.brailleBuffer_,
351       startIndex: startIndex,
352       endIndex: endIndex
353     });
355     if (this.brailleBuffer_)
356       cvox.ChromeVox.braille.write(output);
358     // Display.
359     chrome.accessibilityPrivate.setFocusRing(this.locations_);
360   },
362   /**
363    * Renders the given range using optional context previous range and event
364    * type.
365    * @param {!cursors.Range} range
366    * @param {cursors.Range} prevRange
367    * @param {chrome.automation.EventType|string} type
368    * @param {!cvox.Spannable} buff Buffer to receive rendered output.
369    * @private
370    */
371   render_: function(range, prevRange, type, buff) {
372     if (range.isSubNode())
373       this.subNode_(range, prevRange, type, buff);
374     else
375       this.range_(range, prevRange, type, buff);
376   },
378   /**
379    * Format the node given the format specifier.
380    * @param {!chrome.automation.AutomationNode} node
381    * @param {string|!Object} format The output format either specified as an
382    * output template string or a parsed output format tree.
383    * @param {!Object=} opt_exclude A set of attributes to exclude.
384    * @private
385    */
386   format_: function(node, format, buff, opt_exclude) {
387     opt_exclude = opt_exclude || {};
388     var tokens = [];
389     var args = null;
391     // Hacky way to support args.
392     if (typeof(format) == 'string') {
393       format = format.replace(/([,:])\W/g, '$1');
394       tokens = format.split(' ');
395     } else {
396       tokens = [format];
397     }
399     tokens.forEach(function(token) {
400       // Ignore empty tokens.
401       if (!token)
402         return;
404       // Parse the token.
405       var tree;
406       if (typeof(token) == 'string')
407         tree = this.createParseTree(token);
408       else
409         tree = token;
411       // Obtain the operator token.
412       token = tree.value;
414       // Set suffix options.
415       var options = {};
416       options.ifEmpty = token[token.length - 1] == '=';
417       if (options.ifEmpty)
418         token = token.substring(0, token.length - 1);
420       // Process token based on prefix.
421       var prefix = token[0];
422       token = token.slice(1);
424       if (opt_exclude[token])
425         return;
427       // All possible tokens based on prefix.
428       if (prefix == '$') {
429         options.annotation = token;
430         if (token == 'role') {
431           // Non-localized role and state obtained by default.
432           this.addToSpannable_(buff, node.role, options);
433         } else if (token == 'value') {
434           var text = node.attributes.value;
435           if (text) {
436             var offset = buff.getLength();
437             if (node.attributes.textSelStart !== undefined) {
438               options.annotation = new Output.SelectionSpan(
439                   node.attributes.textSelStart,
440                   node.attributes.textSelEnd);
441             }
442           } else if (node.role == chrome.automation.RoleType.staticText) {
443             // TODO(dtseng): Remove once Blink treats staticText values as
444             // names.
445             text = node.attributes.name;
446           }
447           this.addToSpannable_(buff, text, options);
448         } else if (token == 'indexInParent') {
449           this.addToSpannable_(buff, node.indexInParent + 1);
450         } else if (token == 'parentChildCount') {
451           if (node.parent)
452           this.addToSpannable_(buff, node.parent.children.length);
453         } else if (token == 'state') {
454           Object.getOwnPropertyNames(node.state).forEach(function(s) {
455             this.addToSpannable_(buff, s, options);
456           }.bind(this));
457         } else if (token == 'find') {
458           // Find takes two arguments: JSON query string and format string.
459           if (tree.firstChild) {
460             var jsonQuery = tree.firstChild.value;
461             node = node.find(
462                 /** @type {Object}*/(JSON.parse(jsonQuery)));
463             var formatString = tree.firstChild.nextSibling;
464             if (node)
465               this.format_(node, formatString, buff);
466           }
467         } else if (token == 'descendants') {
468           if (AutomationPredicate.leaf(node))
469             return;
471           // Construct a range to the leftmost and rightmost leaves.
472           var leftmost = AutomationUtil.findNodePre(
473               node, Dir.FORWARD, AutomationPredicate.leaf);
474           var rightmost = AutomationUtil.findNodePre(
475               node, Dir.BACKWARD, AutomationPredicate.leaf);
476           if (!leftmost || !rightmost)
477             return;
479           var subrange = new cursors.Range(
480               new cursors.Cursor(leftmost, 0),
481               new cursors.Cursor(rightmost, 0));
482           this.range_(subrange, null, 'navigate', buff);
483         } else if (node.attributes[token]) {
484           this.addToSpannable_(buff, node.attributes[token], options);
485         } else if (node.state[token]) {
486           this.addToSpannable_(buff, token, options);
487         } else if (tree.firstChild) {
488           // Custom functions.
489           if (token == 'if') {
490             var cond = tree.firstChild;
491             var attrib = cond.value.slice(1);
492             if (node.attributes[attrib] || node.state[attrib])
493               this.format_(node, cond.nextSibling, buff);
494             else
495               this.format_(node, cond.nextSibling.nextSibling, buff);
496           } else if (token == 'earcon') {
497             var contentBuff = new cvox.Spannable();
498             if (tree.firstChild.nextSibling)
499               this.format_(node, tree.firstChild.nextSibling, contentBuff);
500             options.annotation = new Output.Action(function() {
501               cvox.ChromeVox.earcons.playEarcon(
502                   cvox.AbstractEarcons[tree.firstChild.value]);
503             });
504             this.addToSpannable_(buff, contentBuff, options);
505           }
506         }
507       } else if (prefix == '@') {
508         var msgId = token;
509         var msgArgs = [];
510         var curMsg = tree.firstChild;
512         while (curMsg) {
513           var arg = curMsg.value;
514           if (arg[0] != '$') {
515             console.error('Unexpected value: ' + arg);
516             return;
517           }
518           var msgBuff = new cvox.Spannable();
519           this.format_(node, arg, msgBuff);
520           msgArgs.push(msgBuff.toString());
521           curMsg = curMsg.nextSibling;
522         }
523           var msg = cvox.ChromeVox.msgs.getMsg(msgId, msgArgs);
524         try {
525           if (this.formatOptions_.braille)
526             msg = cvox.ChromeVox.msgs.getMsg(msgId + '_brl', msgArgs) || msg;
527         } catch(e) {}
529         if (msg) {
530           this.addToSpannable_(buff, msg, options);
531         }
532       } else if (prefix == '!') {
533         this.speechProperties_[token] = true;
534       }
535     }.bind(this));
536   },
538   /**
539    * @param {!cursors.Range} range
540    * @param {cursors.Range} prevRange
541    * @param {chrome.automation.EventType|string} type
542    * @param {!cvox.Spannable} rangeBuff
543    * @private
544    */
545   range_: function(range, prevRange, type, rangeBuff) {
546     if (!prevRange)
547       prevRange = cursors.Range.fromNode(range.getStart().getNode().root);
549     var cursor = range.getStart();
550     var prevNode = prevRange.getStart().getNode();
552     var formatNodeAndAncestors = function(node, prevNode) {
553       var buff = new cvox.Spannable();
554       this.ancestry_(node, prevNode, type, buff);
555       this.node_(node, prevNode, type, buff);
556       if (this.formatOptions_.location)
557         this.locations_.push(node.location);
558       return buff;
559     }.bind(this);
561     while (cursor.getNode() != range.getEnd().getNode()) {
562       var node = cursor.getNode();
563       this.addToSpannable_(
564           rangeBuff, formatNodeAndAncestors(node, prevNode));
565       prevNode = node;
566       cursor = cursor.move(cursors.Unit.NODE,
567                            cursors.Movement.DIRECTIONAL,
568                            Dir.FORWARD);
569     }
570     var lastNode = range.getEnd().getNode();
571     this.addToSpannable_(rangeBuff, formatNodeAndAncestors(lastNode, prevNode));
572   },
574   /**
575    * @param {!chrome.automation.AutomationNode} node
576    * @param {!chrome.automation.AutomationNode} prevNode
577    * @param {chrome.automation.EventType|string} type
578    * @param {!cvox.Spannable} buff
579    * @param {!Object=} opt_exclude A list of attributes to exclude from
580    * processing.
581    * @private
582    */
583   ancestry_: function(node, prevNode, type, buff, opt_exclude) {
584     opt_exclude = opt_exclude || {};
585     var prevUniqueAncestors =
586         AutomationUtil.getUniqueAncestors(node, prevNode);
587     var uniqueAncestors = AutomationUtil.getUniqueAncestors(prevNode, node);
589     // First, look up the event type's format block.
590     // Navigate is the default event.
591     var eventBlock = Output.RULES[type] || Output.RULES['navigate'];
593     for (var i = 0, formatPrevNode;
594          (formatPrevNode = prevUniqueAncestors[i]);
595          i++) {
596       var roleBlock = eventBlock[formatPrevNode.role] || eventBlock['default'];
597       if (roleBlock.leave)
598         this.format_(formatPrevNode, roleBlock.leave, buff, opt_exclude);
599     }
601     var enterOutput = [];
602     var enterRole = {};
603     for (var j = uniqueAncestors.length - 2, formatNode;
604          (formatNode = uniqueAncestors[j]);
605          j--) {
606       var roleBlock = eventBlock[formatNode.role] || eventBlock['default'];
607       if (roleBlock.enter) {
608         if (enterRole[formatNode.role])
609           continue;
610         enterRole[formatNode.role] = true;
611         var tempBuff = new cvox.Spannable('');
612         this.format_(formatNode, roleBlock.enter, tempBuff, opt_exclude);
613         enterOutput.unshift(tempBuff);
614       }
615     }
616     enterOutput.forEach(function(c) {
617       this.addToSpannable_(buff, c);
618     }.bind(this));
620     if (!opt_exclude.stay) {
621       var commonFormatNode = uniqueAncestors[0];
622       while (commonFormatNode && commonFormatNode.parent) {
623         commonFormatNode = commonFormatNode.parent;
624         var roleBlock =
625             eventBlock[commonFormatNode.role] || eventBlock['default'];
626         if (roleBlock.stay)
627           this.format_(commonFormatNode, roleBlock.stay, buff, opt_exclude);
628       }
629     }
630   },
632   /**
633    * @param {!chrome.automation.AutomationNode} node
634    * @param {!chrome.automation.AutomationNode} prevNode
635    * @param {chrome.automation.EventType|string} type
636    * @param {!cvox.Spannable} buff
637    * @private
638    */
639   node_: function(node, prevNode, type, buff) {
640     // Navigate is the default event.
641     var eventBlock = Output.RULES[type] || Output.RULES['navigate'];
642     var roleBlock = eventBlock[node.role] || eventBlock['default'];
643     var speakFormat = roleBlock.speak || eventBlock['default'].speak;
644     this.format_(node, speakFormat, buff);
645   },
647   /**
648    * @param {!cursors.Range} range
649    * @param {cursors.Range} prevRange
650    * @param {chrome.automation.EventType|string} type
651    * @param {!cvox.Spannable} buff
652    * @private
653    */
654   subNode_: function(range, prevRange, type, buff) {
655     if (!prevRange)
656       prevRange = range;
657     var dir = cursors.Range.getDirection(prevRange, range);
658     var prevNode = prevRange.getBound(dir).getNode();
659     this.ancestry_(
660         range.getStart().getNode(), prevNode, type, buff,
661         {stay: true, name: true, value: true});
662     var startIndex = range.getStart().getIndex();
663     var endIndex = range.getEnd().getIndex();
664     if (startIndex === endIndex)
665       endIndex++;
666     this.addToSpannable_(
667         buff, range.getStart().getText().substring(startIndex, endIndex));
668   },
670   /**
671    * Adds to the given buffer with proper delimiters added.
672    * @param {!cvox.Spannable} spannable
673    * @param {string|!cvox.Spannable} value
674    * @param {{ifEmpty: boolean,
675    *      annotation: (string|Output.Action|undefined)}=} opt_options
676    */
677   addToSpannable_: function(spannable, value, opt_options) {
678     opt_options = opt_options || {ifEmpty: false, annotation: undefined};
679     if ((!value || value.length == 0) && !opt_options.annotation)
680       return;
682     var spannableToAdd = new cvox.Spannable(value, opt_options.annotation);
683     if (spannable.getLength() == 0) {
684       spannable.append(spannableToAdd);
685       return;
686     }
688     if (opt_options.ifEmpty &&
689         opt_options.annotation &&
690         (spannable.getSpanStart(opt_options.annotation) != undefined ||
691             spannable.getSpanStart(
692                 Output.ATTRIBUTE_ALIAS[opt_options.annotation]) != undefined))
693       return;
695     var prefixed = new cvox.Spannable(Output.SPACE);
696     prefixed.append(spannableToAdd);
697     spannable.append(prefixed);
698   },
700   /**
701    * Parses the token containing a custom function and returns a tree.
702    * @param {string} inputStr
703    * @return {Object}
704    */
705   createParseTree: function(inputStr) {
706     var root = {value: ''};
707     var currentNode = root;
708     var index = 0;
709     var braceNesting = 0;
710     while (index < inputStr.length) {
711       if (inputStr[index] == '(') {
712         currentNode.firstChild = {value: ''};
713         currentNode.firstChild.parent = currentNode;
714         currentNode = currentNode.firstChild;
715       } else if (inputStr[index] == ')') {
716         currentNode = currentNode.parent;
717       } else if (inputStr[index] == '{') {
718         braceNesting++;
719         currentNode.value += inputStr[index];
720       } else if (inputStr[index] == '}') {
721         braceNesting--;
722         currentNode.value += inputStr[index];
723       } else if (inputStr[index] == ',' && braceNesting === 0) {
724         currentNode.nextSibling = {value: ''};
725         currentNode.nextSibling.parent = currentNode.parent;
726         currentNode = currentNode.nextSibling;
727       } else {
728         currentNode.value += inputStr[index];
729       }
730       index++;
731     }
733     if (currentNode != root)
734       throw 'Unbalanced parenthesis.';
736     return root;
737   }
740 });  // goog.scope