Add new certificateProvider extension API.
[chromium-blink-merge.git] / chrome / browser / resources / chromeos / chromevox / common / description_util.js
blobe14def44141d3e990eb66d8a9bea7fb88ca7f622
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 A utility class for building NavDescriptions from the dom.
7  */
10 goog.provide('cvox.DescriptionUtil');
12 goog.require('cvox.AriaUtil');
13 goog.require('cvox.AuralStyleUtil');
14 goog.require('cvox.BareObjectWalker');
15 goog.require('cvox.CursorSelection');
16 goog.require('cvox.DomUtil');
17 goog.require('cvox.EarconUtil');
18 goog.require('cvox.MathmlStore');
19 goog.require('cvox.NavDescription');
20 goog.require('cvox.SpeechRuleEngine');
21 goog.require('cvox.TraverseMath');
24 /**
25  * Lists all Node tagName's who's description is derived from its subtree.
26  * @type {Object<boolean>}
27  */
28 cvox.DescriptionUtil.COLLECTION_NODE_TYPE = {
29   'H1': true,
30   'H2': true,
31   'H3': true,
32   'H4': true,
33   'H5': true,
34   'H6': true
37 /**
38  * Get a control's complete description in the same format as if you
39  *     navigated to the node.
40  * @param {Element} control A control.
41  * @param {Array<Node>=} opt_changedAncestors The changed ancestors that will
42  * be used to determine what needs to be spoken. If this is not provided, the
43  * ancestors used to determine what needs to be spoken will just be the control
44  * itself and its surrounding control if it has one.
45  * @return {cvox.NavDescription} The description of the control.
46  */
47 cvox.DescriptionUtil.getControlDescription =
48     function(control, opt_changedAncestors) {
49   var ancestors = [control];
50   if (opt_changedAncestors && (opt_changedAncestors.length > 0)) {
51     ancestors = opt_changedAncestors;
52   } else {
53     var surroundingControl = cvox.DomUtil.getSurroundingControl(control);
54     if (surroundingControl) {
55       ancestors = [surroundingControl, control];
56     }
57   }
59   var description = cvox.DescriptionUtil.getDescriptionFromAncestors(
60       ancestors, true, cvox.VERBOSITY_VERBOSE);
62   // Use heuristics if the control doesn't otherwise have a name.
63   if (surroundingControl) {
64     var name = cvox.DomUtil.getName(surroundingControl);
65     if (name.length == 0) {
66       name = cvox.DomUtil.getControlLabelHeuristics(surroundingControl);
67       if (name.length > 0) {
68         description.context = name + ' ' + description.context;
69       }
70     }
71   } else {
72     var name = cvox.DomUtil.getName(control);
73     if (name.length == 0) {
74       name = cvox.DomUtil.getControlLabelHeuristics(control);
75       if (name.length > 0) {
76         description.text = cvox.DomUtil.collapseWhitespace(name);
77       }
78     }
79     var value = cvox.DomUtil.getValue(control);
80     if (value.length > 0) {
81       description.userValue = cvox.DomUtil.collapseWhitespace(value);
82     }
83   }
85   return description;
89 /**
90  * Returns a description of a navigation from an array of changed
91  * ancestor nodes. The ancestors are in order from the highest in the
92  * tree to the lowest, i.e. ending with the current leaf node.
93  *
94  * @param {Array<Node>} ancestorsArray An array of ancestor nodes.
95  * @param {boolean} recursive Whether or not the element's subtree should
96  *     be used; true by default.
97  * @param {number} verbosity The verbosity setting.
98  * @return {cvox.NavDescription} The description of the navigation action.
99  */
100 cvox.DescriptionUtil.getDescriptionFromAncestors = function(
101     ancestorsArray, recursive, verbosity) {
102   if (typeof(recursive) === 'undefined') {
103     recursive = true;
104   }
105   var len = ancestorsArray.length;
106   var context = '';
107   var text = '';
108   var userValue = '';
109   var annotation = '';
110   var earcons = [];
111   var personality = null;
112   var hint = '';
114   if (len > 0) {
115     text = cvox.DomUtil.getName(ancestorsArray[len - 1], recursive);
117     userValue = cvox.DomUtil.getValue(ancestorsArray[len - 1]);
118   }
119   for (var i = len - 1; i >= 0; i--) {
120     var node = ancestorsArray[i];
122     hint = cvox.DomUtil.getHint(node);
124     // Don't speak dialogs here, they're spoken when events occur.
125     var role = node.getAttribute ? node.getAttribute('role') : null;
126     if (role == 'alertdialog') {
127       continue;
128     }
130     var roleText = cvox.DomUtil.getRole(node, verbosity);
132     // Use the ancestor closest to the target to be the personality.
133     if (!personality) {
134       personality = cvox.AuralStyleUtil.getStyleForNode(node);
135     }
136     // TODO(dtseng): Is this needed?
137     if (i < len - 1 && node.hasAttribute('role')) {
138       var name = cvox.DomUtil.getName(node, false);
139       if (name) {
140         roleText = name + ' ' + roleText;
141       }
142     }
143     if (roleText.length > 0) {
144       // Since we prioritize reading of context in reading order, only populate
145       // it for larger ancestry changes.
146       if (context.length > 0 ||
147           (annotation.length > 0 && node.childElementCount > 1)) {
148         context = roleText + ' ' + cvox.DomUtil.getState(node, false) +
149                   ' ' + context;
150       } else {
151         if (annotation.length > 0) {
152           annotation +=
153               ' ' + roleText + ' ' + cvox.DomUtil.getState(node, true);
154         } else {
155           annotation = roleText + ' ' + cvox.DomUtil.getState(node, true);
156         }
157       }
158     }
159     var earcon = cvox.EarconUtil.getEarcon(node);
160     if (earcon != null && earcons.indexOf(earcon) == -1) {
161       earcons.push(earcon);
162     }
163   }
164   return new cvox.NavDescription({
165     context: cvox.DomUtil.collapseWhitespace(context),
166     text: cvox.DomUtil.collapseWhitespace(text),
167     userValue: cvox.DomUtil.collapseWhitespace(userValue),
168     annotation: cvox.DomUtil.collapseWhitespace(annotation),
169     earcons: earcons,
170     personality: personality,
171     hint: cvox.DomUtil.collapseWhitespace(hint)
172   });
176  * Returns a description of a navigation from an array of changed
177  * ancestor nodes. The ancestors are in order from the highest in the
178  * tree to the lowest, i.e. ending with the current leaf node.
180  * @param {Node} prevNode The previous node in navigation.
181  * @param {Node} node The current node in navigation.
182  * @param {boolean} recursive Whether or not the element's subtree should
183  *     be used; true by default.
184  * @param {number} verbosity The verbosity setting.
185  * @return {!Array<cvox.NavDescription>} The description of the navigation
186  * action.
187  */
188 cvox.DescriptionUtil.getDescriptionFromNavigation =
189     function(prevNode, node, recursive, verbosity) {
190   if (!prevNode || !node) {
191     return [];
192   }
194   // Specialized math descriptions.
195   if (cvox.DomUtil.isMath(node) &&
196       !cvox.AriaUtil.isMath(node)) {
197     return cvox.DescriptionUtil.getMathDescription(node);
198   }
200   // Next, check to see if the current node is a collection type.
201   if (cvox.DescriptionUtil.COLLECTION_NODE_TYPE[node.tagName]) {
202     return cvox.DescriptionUtil.getCollectionDescription(
203         /** @type {!cvox.CursorSelection} */(
204             cvox.CursorSelection.fromNode(prevNode)),
205         /** @type {!cvox.CursorSelection} */(
206             cvox.CursorSelection.fromNode(node)));
207   }
209   // Now, generate a description for all other elements.
210   var ancestors = cvox.DomUtil.getUniqueAncestors(prevNode, node, true);
211   var desc = cvox.DescriptionUtil.getDescriptionFromAncestors(
212       ancestors, recursive, verbosity);
213   var prevAncestors = cvox.DomUtil.getUniqueAncestors(node, prevNode);
214   if (cvox.DescriptionUtil.shouldDescribeExit_(prevAncestors)) {
215     var prevDesc = cvox.DescriptionUtil.getDescriptionFromAncestors(
216         prevAncestors, recursive, verbosity);
217     if (prevDesc.context && !desc.context) {
218       desc.context =
219           cvox.ChromeVox.msgs.getMsg('exited_container', [prevDesc.context]);
220     }
221   }
222   return [desc];
227  * Returns an array of NavDescriptions that includes everything that would be
228  * spoken by an object walker while traversing from prevSel to sel.
229  * It also includes any necessary annotations and context about the set of
230  * descriptions. This function is here because most (currently all) walkers
231  * that iterate over non-leaf nodes need this sort of description.
232  * This is an awkward design, and should be changed in the future.
233  * @param {!cvox.CursorSelection} prevSel The previous selection.
234  * @param {!cvox.CursorSelection} sel The selection.
235  * @return {!Array<!cvox.NavDescription>} The descriptions as described above.
236  */
237 cvox.DescriptionUtil.getCollectionDescription = function(prevSel, sel) {
238   var descriptions = cvox.DescriptionUtil.getRawDescriptions_(prevSel, sel);
239   cvox.DescriptionUtil.insertCollectionDescription_(descriptions);
240   return descriptions;
245  * Used for getting collection descriptions.
246  * @type {!cvox.BareObjectWalker}
247  * @private
248  */
249 cvox.DescriptionUtil.subWalker_ = new cvox.BareObjectWalker();
253  * Returns the descriptions that would be gotten by an object walker.
254  * @param {!cvox.CursorSelection} prevSel The previous selection.
255  * @param {!cvox.CursorSelection} sel The selection.
256  * @return {!Array<!cvox.NavDescription>} The descriptions.
257  * @private
258  */
259 cvox.DescriptionUtil.getRawDescriptions_ = function(prevSel, sel) {
260   // Use a object walker in non-smart mode to traverse all of the
261   // nodes inside the current smart node and return their annotations.
262   var descriptions = [];
264   // We want the descriptions to be in forward order whether or not the
265   // selection is reversed.
266   sel = sel.clone().setReversed(false);
267   var node = cvox.DescriptionUtil.subWalker_.sync(sel).start.node;
269   var prevNode = prevSel.end.node;
270   var curSel = cvox.CursorSelection.fromNode(node);
272   if (!curSel) {
273     return [];
274   }
276   while (cvox.DomUtil.isDescendantOfNode(node, sel.start.node)) {
277     var ancestors = cvox.DomUtil.getUniqueAncestors(prevNode, node);
278     // Specialized math descriptions.
279     if (cvox.DomUtil.isMath(node) &&
280         !cvox.AriaUtil.isMath(node)) {
281       descriptions =
282           descriptions.concat(cvox.DescriptionUtil.getMathDescription(node));
283     } else {
284       var description = cvox.DescriptionUtil.getDescriptionFromAncestors(
285           ancestors, true, cvox.ChromeVox.verbosity);
286       descriptions.push(description);
287     }
288     curSel = cvox.DescriptionUtil.subWalker_.next(curSel);
289     if (!curSel) {
290       break;
291     }
293     curSel = /** @type {!cvox.CursorSelection} */ (curSel);
294     prevNode = node;
295     node = curSel.start.node;
296   }
298   return descriptions;
302  * Returns the full descriptions of the child nodes that would be gotten by an
303  * object walker.
304  * @param {?Element} prevnode The previous element if there is one.
305  * @param {!Element} node The target element.
306  * @return {!Array<!cvox.NavDescription>} The descriptions.
307  */
308 cvox.DescriptionUtil.getFullDescriptionsFromChildren =
309     function(prevnode, node) {
310   var descriptions = [];
311   if (!node) {
312     return descriptions;
313   }
314   var desc;
315   if (cvox.DomUtil.isLeafNode(node)) {
316     var ancestors;
317     if (prevnode) {
318       ancestors = cvox.DomUtil.getUniqueAncestors(prevnode, node);
319     } else {
320       ancestors = new Array();
321       ancestors.push(node);
322     }
323     desc = cvox.DescriptionUtil.getDescriptionFromAncestors(
324         ancestors, true, cvox.ChromeVox.verbosity);
325     descriptions.push(desc);
326     return descriptions;
327   }
328   var originalNode = node;
329   var curSel = cvox.CursorSelection.fromNode(node);
330   if (!curSel) {
331     return descriptions;
332   }
333   node = cvox.DescriptionUtil.subWalker_.sync(curSel).start.node;
334   curSel = cvox.CursorSelection.fromNode(node);
335   if (!curSel) {
336     return descriptions;
337   }
338   while (cvox.DomUtil.isDescendantOfNode(node, originalNode)) {
339     descriptions = descriptions.concat(
340         cvox.DescriptionUtil.getFullDescriptionsFromChildren(prevnode, node));
341     curSel = cvox.DescriptionUtil.subWalker_.next(curSel);
342     if (!curSel) {
343       break;
344     }
345     curSel = /** @type {!cvox.CursorSelection} */ (curSel);
346     prevnode = node;
347     node = curSel.start.node;
348   }
349   return descriptions;
354  * Modify the descriptions to say that it is a collection.
355  * @param {Array<cvox.NavDescription>} descriptions The descriptions.
356  * @private
357  */
358 cvox.DescriptionUtil.insertCollectionDescription_ = function(descriptions) {
359   var annotations = cvox.DescriptionUtil.getAnnotations_(descriptions);
360   // If all of the items have the same annotation, describe it as a
361   // <annotation> collection with <n> items. Currently only enabled
362   // for links, but support should be added for any other type that
363   // makes sense.
364   if (descriptions.length >= 3 &&
365       descriptions[0].context.length == 0 &&
366       annotations.length == 1 &&
367       annotations[0].length > 0 &&
368       cvox.DescriptionUtil.isAnnotationCollection_(annotations[0])) {
369     var commonAnnotation = annotations[0];
370     var firstContext = descriptions[0].context;
371     descriptions[0].context = '';
372     for (var i = 0; i < descriptions.length; i++) {
373       descriptions[i].annotation = '';
374     }
376     descriptions.splice(0, 0, new cvox.NavDescription({
377       context: firstContext,
378       text: '',
379       annotation: cvox.ChromeVox.msgs.getMsg(
380           'collection',
381           [commonAnnotation,
382            cvox.ChromeVox.msgs.getNumber(descriptions.length)])
383     }));
384   }
389  * Pulls the annotations from a description array.
390  * @param {Array<cvox.NavDescription>} descriptions The descriptions.
391  * @return {Array<string>} The annotations.
392  * @private
393  */
394 cvox.DescriptionUtil.getAnnotations_ = function(descriptions) {
395   var annotations = [];
396   for (var i = 0; i < descriptions.length; ++i) {
397     var description = descriptions[i];
398     if (annotations.indexOf(description.annotation) == -1) {
399       // If we have an Internal link collection, call it Link collection.
400       // NOTE(deboer): The message comparison is a symptom of a bad design.
401       // I suspect this code belongs elsewhere but I don't know where, yet.
402       var linkMsg = cvox.ChromeVox.msgs.getMsg('tag_link');
403       if (description.annotation.toLowerCase().indexOf(linkMsg.toLowerCase()) !=
404           -1) {
405         if (annotations.indexOf(linkMsg) == -1) {
406           annotations.push(linkMsg);
407         }
408       } else {
409         annotations.push(description.annotation);
410       }
411     }
412   }
413   return annotations;
418  * Returns true if this annotation should be grouped as a collection,
419  * meaning that instead of repeating the annotation for each item, we
420  * just announce <annotation> collection with <n> items at the front.
422  * Currently enabled for links, but could be extended to support other
423  * roles that make sense.
425  * @param {string} annotation The annotation text.
426  * @return {boolean} If this annotation should be a collection.
427  * @private
428  */
429 cvox.DescriptionUtil.isAnnotationCollection_ = function(annotation) {
430   return (annotation == cvox.ChromeVox.msgs.getMsg('tag_link'));
434  * Determines whether to describe the exit of an ancestor chain.
435  * @param {Array<Node>} ancestors The ancestors exited during navigation.
436  * @return {boolean} The result.
437  * @private
438  */
439 cvox.DescriptionUtil.shouldDescribeExit_ = function(ancestors) {
440   return ancestors.some(function(node) {
441     switch (node.tagName) {
442       case 'TABLE':
443       case 'MATH':
444         return true;
445     }
446     return cvox.AriaUtil.isLandmark(node);
447   });
451 // TODO(sorge): Bad naming...this thing returns *multiple* descriptions.
453  * Generates a description for a math node.
454  * @param {!Node} node The given node.
455  * @return {!Array<cvox.NavDescription>} A list of Navigation descriptions.
456  */
457 cvox.DescriptionUtil.getMathDescription = function(node) {
458   // TODO (sorge) This function should evantually be removed. Descriptions
459   //     should come directly from the speech rule engine, taking information on
460   //     verbosity etc. into account.
461   var speechEngine = cvox.SpeechRuleEngine.getInstance();
462   var traverse = cvox.TraverseMath.getInstance();
463   speechEngine.parameterize(cvox.MathmlStore.getInstance());
464   traverse.initialize(node);
465   var ret = speechEngine.evaluateNode(traverse.activeNode);
466   if (ret == []) {
467     return [new cvox.NavDescription({'text': 'empty math'})];
468   }
469   if (cvox.ChromeVox.verbosity == cvox.VERBOSITY_VERBOSE) {
470     ret[ret.length - 1].annotation = 'math';
471   }
472   ret[0].pushEarcon(cvox.Earcon.SPECIAL_CONTENT);
473   return ret;