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.
4
5/**
6 * @fileoverview A utility class for building NavDescriptions from the dom.
7 */
8
9
10goog.provide('cvox.DescriptionUtil');
11
12goog.require('cvox.AriaUtil');
13goog.require('cvox.AuralStyleUtil');
14goog.require('cvox.BareObjectWalker');
15goog.require('cvox.CursorSelection');
16goog.require('cvox.DomUtil');
17goog.require('cvox.EarconUtil');
18goog.require('cvox.MathmlStore');
19goog.require('cvox.NavDescription');
20goog.require('cvox.SpeechRuleEngine');
21goog.require('cvox.TraverseMath');
22
23
24/**
25 * Lists all Node tagName's who's description is derived from its subtree.
26 * @type {Object.<string, boolean>}
27 */
28cvox.DescriptionUtil.COLLECTION_NODE_TYPE = {
29  'H1': true,
30  'H2': true,
31  'H3': true,
32  'H4': true,
33  'H5': true,
34  'H6': true
35};
36
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 */
47cvox.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  }
58
59  var description = cvox.DescriptionUtil.getDescriptionFromAncestors(
60      ancestors, true, cvox.VERBOSITY_VERBOSE);
61
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  }
84
85  return description;
86};
87
88
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 */
100cvox.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 = '';
113
114  if (len > 0) {
115    text = cvox.DomUtil.getName(ancestorsArray[len - 1], recursive);
116
117    userValue = cvox.DomUtil.getValue(ancestorsArray[len - 1]);
118  }
119  for (var i = len - 1; i >= 0; i--) {
120    var node = ancestorsArray[i];
121
122    hint = cvox.DomUtil.getHint(node);
123
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    }
129
130    var roleText = cvox.DomUtil.getRole(node, verbosity);
131
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  });
173};
174
175/**
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.
179 *
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 */
188cvox.DescriptionUtil.getDescriptionFromNavigation =
189    function(prevNode, node, recursive, verbosity) {
190  if (!prevNode || !node) {
191    return [];
192  }
193
194  // Specialized math descriptions.
195  if (cvox.DomUtil.isMath(node) &&
196      !cvox.AriaUtil.isMath(node)) {
197    return cvox.DescriptionUtil.getMathDescription(node);
198  }
199
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  }
208
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];
223};
224
225
226/**
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 */
237cvox.DescriptionUtil.getCollectionDescription = function(prevSel, sel) {
238  var descriptions = cvox.DescriptionUtil.getRawDescriptions_(prevSel, sel);
239  cvox.DescriptionUtil.insertCollectionDescription_(descriptions);
240  return descriptions;
241};
242
243
244/**
245 * Used for getting collection descriptions.
246 * @type {!cvox.BareObjectWalker}
247 * @private
248 */
249cvox.DescriptionUtil.subWalker_ = new cvox.BareObjectWalker();
250
251
252/**
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 */
259cvox.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 = [];
263
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;
268
269  var prevNode = prevSel.end.node;
270  var curSel = cvox.CursorSelection.fromNode(node);
271
272  if (!curSel) {
273    return [];
274  }
275
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    }
292
293    curSel = /** @type {!cvox.CursorSelection} */ (curSel);
294    prevNode = node;
295    node = curSel.start.node;
296  }
297
298  return descriptions;
299};
300
301/**
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 */
308cvox.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;
350};
351
352
353/**
354 * Modify the descriptions to say that it is a collection.
355 * @param {Array.<cvox.NavDescription>} descriptions The descriptions.
356 * @private
357 */
358cvox.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    }
375
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  }
385};
386
387
388/**
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 */
394cvox.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;
414};
415
416
417/**
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.
421 *
422 * Currently enabled for links, but could be extended to support other
423 * roles that make sense.
424 *
425 * @param {string} annotation The annotation text.
426 * @return {boolean} If this annotation should be a collection.
427 * @private
428 */
429cvox.DescriptionUtil.isAnnotationCollection_ = function(annotation) {
430  return (annotation == cvox.ChromeVox.msgs.getMsg('tag_link'));
431};
432
433/**
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 */
439cvox.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  });
448};
449
450
451// TODO(sorge): Bad naming...this thing returns *multiple* descriptions.
452/**
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 */
457cvox.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.AbstractEarcons.SPECIAL_CONTENT);
473  return ret;
474};
475