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
5goog.provide('cvox.TraverseMath');
6
7goog.require('cvox.ChromeVox');
8goog.require('cvox.DomUtil');
9goog.require('cvox.SemanticTree');
10
11
12/**
13 * Initializes the traversal with the provided math node.
14 *
15 * @constructor
16 */
17cvox.TraverseMath = function() {
18  /**
19   * The active math <MATH> node. In this context, "active" means that this is
20   * the math expression the TraverseMath object is navigating.
21   * @type {Node}
22   */
23  this.activeMath = null;
24
25  /**
26   * The node currently under inspection.
27   * @type {Node}
28   */
29  this.activeNode = null;
30
31  /**
32   * Dictionary of all LaTeX elements in the page if there are any.
33   * @type {!Object.<string, !Node>}
34   * @private
35   */
36  this.allTexs_ = {};
37
38  /**
39   * Dictionary of all MathJaxs elements in the page if there are any.
40   * @type {!Object.<string, !Node>}
41   * @private
42   */
43  this.allMathjaxs_ = {};
44
45  /**
46   * Dictionary of all MathJaxs elements that have not yet been translated at
47   * page load or during MathJax rendering.
48   * @type {!Object.<string, !Node>}
49   * @private
50   */
51  this.todoMathjaxs_ = {};
52
53  /**
54   * When traversing a Mathjax node this will contain the internal
55   * MathML representation of the node.
56   * @type {Node}
57   */
58  this.activeMathmlHost = null;
59
60  /**
61   * Semantic representation of the current node.
62   * @type {Node}
63   */
64  this.activeSemanticHost = null;
65
66  /**
67   * List of domain names.
68   * @type {Array.<string>}
69   */
70  this.allDomains = [];
71
72  /**
73   * List of style names.
74   * @type {Array.<string>}
75   */
76  this.allStyles = [];
77
78  /**
79   * Current domain.
80   * @type {string}
81   */
82  this.domain = 'default';
83
84  /**
85   * Current style.
86   * @type {string}
87   */
88  this.style = 'short';
89
90  /**
91   * Initialize special objects if necessary.
92   */
93  if (cvox.ChromeVox.mathJax) {
94    this.initializeMathjaxs();
95    this.initializeAltMaths();
96  }
97};
98goog.addSingletonGetter(cvox.TraverseMath);
99
100
101/**
102 * @type {boolean}
103 * @private
104 */
105cvox.TraverseMath.setSemantic_ = false;
106
107
108/**
109 * Toggles the semantic setting.
110 * @return {boolean} True if semantic interpretation is switched on. False
111 *     otherwise.
112 */
113cvox.TraverseMath.toggleSemantic = function() {
114  return cvox.TraverseMath.setSemantic_ = !cvox.TraverseMath.setSemantic_;
115};
116
117
118/**
119 * Initializes a traversal of a math expression.
120 * @param {Node} node A MathML node.
121 */
122cvox.TraverseMath.prototype.initialize = function(node) {
123  if (cvox.DomUtil.isMathImg(node)) {
124    // If a node has a cvoxid attribute we know that it contains a LaTeX
125    // expression that we have rewritten into its corresponding MathML
126    // representation, which we can speak and walk.
127    if (!node.hasAttribute('cvoxid')) {
128      return;
129    }
130    var cvoxid = node.getAttribute('cvoxid');
131    node = this.allTexs_[cvoxid];
132  }
133  if (cvox.DomUtil.isMathJax(node)) {
134      this.activeMathmlHost = this.allMathjaxs_[node.getAttribute('id')];
135  }
136  this.activeMath = this.activeMathmlHost || node;
137  this.activeNode = this.activeMathmlHost || node;
138  if (this.activeNode && cvox.TraverseMath.setSemantic_ &&
139      this.activeNode.nodeType == Node.ELEMENT_NODE) {
140    this.activeNode =
141        (new cvox.SemanticTree(/** @type {!Element} */ (this.activeNode))).xml();
142  }
143};
144
145
146/**
147 * Adds a mapping of a MathJax node to its MathML representation to the
148 * dictionary of MathJax elements.
149 * @param {!Node} mml The MathML node.
150 * @param {string} id The MathJax node id.
151 */
152cvox.TraverseMath.prototype.addMathjax = function(mml, id) {
153  var spanId = cvox.DomUtil.getMathSpanId(id);
154  if (spanId) {
155    this.allMathjaxs_[spanId] = mml;
156  } else {
157    this.redoMathjaxs(mml, id);
158  }
159};
160
161
162/**
163 * Retries to compute MathML representations of MathJax elements, if
164 * they have not been filled in during rendering.
165 * @param {!Node} mml The MathML node.
166 * @param {string} id The MathJax node id.
167 */
168cvox.TraverseMath.prototype.redoMathjaxs = function(mml, id) {
169  var fetch = goog.bind(function() {this.addMathjax(mml, id);}, this);
170  setTimeout(fetch, 500);
171};
172
173
174/**
175 * Initializes the MathJax to MathML mapping.
176 * We first try to get all MathJax elements that are already being rendered.
177 * Secondly, we register a signal to get updated on all elements that are
178 * rendered or re-rendered later.
179 */
180cvox.TraverseMath.prototype.initializeMathjaxs = function() {
181  var callback =
182      goog.bind(function(mml, id) {
183                  this.addMathjax(mml, id);
184                }, this);
185  cvox.ChromeVox.mathJax.isMathjaxActive(
186      function(bool) {
187        if (bool) {
188          cvox.ChromeVox.mathJax.getAllJax(callback);
189          cvox.ChromeVox.mathJax.registerSignal(callback, 'New Math');
190        }
191      });
192};
193
194
195/**
196 * Initializes the elements in the page that we identify as potentially
197 * containing tex or asciimath alt text.
198 */
199cvox.TraverseMath.prototype.initializeAltMaths = function() {
200  if (!document.querySelector(
201      cvox.DomUtil.altMathQuerySelector('tex') + ', ' +
202          cvox.DomUtil.altMathQuerySelector('asciimath'))) {
203    return;
204  }
205  var callback = goog.bind(
206      function(mml, id) {
207        this.allTexs_[id] = mml;
208      }, this);
209  // Inject a minimalistic version of MathJax into the page.
210  cvox.ChromeVox.mathJax.injectScripts();
211  // Once MathJax is injected we harvest all Latex and AsciiMath in alt
212  // attributes and translate them to MathML expression.
213  cvox.ChromeVox.mathJax.isMathjaxActive(
214      function(active) {
215        if (active) {
216          cvox.ChromeVox.mathJax.configMediaWiki();
217          cvox.ChromeVox.mathJax.getAllTexs(callback);
218          cvox.ChromeVox.mathJax.getAllAsciiMaths(callback);
219        }
220      });
221};
222
223
224/**
225 * Moves to the next leaf node in the current Math expression if it exists.
226 * @param {boolean} reverse True if reversed. False by default.
227 * @param {function(!Node):boolean} pred Predicate deciding what a leaf is.
228 * @return {Node} The next node.
229 */
230cvox.TraverseMath.prototype.nextLeaf = function(reverse, pred) {
231  if (this.activeNode && this.activeMath) {
232    var next = pred(this.activeNode) ?
233      cvox.DomUtil.directedFindNextNode(
234          this.activeNode, this.activeMath, reverse, pred) :
235      cvox.DomUtil.directedFindFirstNode(this.activeNode, reverse, pred);
236    if (next) {
237      this.activeNode = next;
238    }
239  }
240  return this.activeNode;
241};
242
243
244// TODO (sorge) Refactor this logic into single walkers.
245/**
246 * Returns a string with the content of the active node.
247 * @return {string} The active content.
248 */
249cvox.TraverseMath.prototype.activeContent = function() {
250  return this.activeNode.textContent;
251};
252
253
254/**
255 * Moves to the next subtree from a given node in a depth first fashion.
256 * @param {boolean} reverse True if reversed. False by default.
257 * @param {function(!Node):boolean} pred Predicate deciding what a subtree is.
258 * @return {Node} The next subtree.
259 */
260cvox.TraverseMath.prototype.nextSubtree = function(reverse, pred) {
261  if (!this.activeNode || !this.activeMath) {
262    return null;
263  }
264  if (!reverse) {
265    var child = cvox.DomUtil.directedFindFirstNode(
266        this.activeNode, reverse, pred);
267    if (child) {
268      this.activeNode = child;
269    } else {
270      var next = cvox.DomUtil.directedFindNextNode(
271          this.activeNode, this.activeMath, reverse, pred);
272      if (next) {
273          this.activeNode = next;
274      }
275    }
276  } else {
277    if (this.activeNode == this.activeMath) {
278      var child = cvox.DomUtil.directedFindDeepestNode(
279          this.activeNode, reverse, pred);
280      if (child != this.activeNode) {
281        this.activeNode = child;
282        return this.activeNode;
283      }
284    }
285    var prev = cvox.DomUtil.directedFindNextNode(
286      this.activeNode, this.activeMath, reverse, pred, true, true);
287    if (prev) {
288      this.activeNode = prev;
289    }
290  }
291  return this.activeNode;
292};
293
294
295/**
296 * left or right in the math expression.
297 * Navigation is bounded by the presence of a sibling.
298 * @param {boolean} r True to move left; false to move right.
299 * @return {Node} The result.
300 */
301cvox.TraverseMath.prototype.nextSibling = function(r) {
302  if (!this.activeNode || !this.activeMath) {
303    return null;
304  }
305  var node = this.activeNode;
306      node = r ? node.previousSibling : node.nextSibling;
307  if (!node) {
308    return null;
309  }
310  this.activeNode = node;
311  return this.activeNode;
312};
313
314
315/**
316 * Moves up or down the math expression.
317 * Navigation is bounded by the root math expression.
318 * @param {boolean} r True to move up; false to move down.
319 * @return {Node} The result.
320 */
321cvox.TraverseMath.prototype.nextParentChild = function(r) {
322  if (!this.activeNode || !this.activeMath) {
323    return null;
324  }
325  if (this.activeNode == this.activeMath && r) {
326    return null;
327  }
328  var node = this.activeNode;
329  node = r ? node.parentNode : node.firstChild;
330  if (!node) {
331    return null;
332  }
333  this.activeNode = node;
334  return this.activeNode;
335};
336
337
338/**
339 * Adds a list of domains and styles to the existing one.
340 * @param {Array.<string>} domains List of domain names.
341 * @param {Array.<string>} styles List of style names.
342 */
343cvox.TraverseMath.prototype.addDomainsAndStyles = function(domains, styles) {
344  this.allDomains.push.apply(
345      this.allDomains,
346      domains.filter(
347          goog.bind(function(x) {return this.allDomains.indexOf(x) < 0;},
348                    this)));
349  this.allStyles.push.apply(
350      this.allStyles,
351      styles.filter(
352          goog.bind(function(x) {return this.allStyles.indexOf(x) < 0;},
353                    this)));
354};
355
356
357/**
358 * Gets a list of domains and styles from the symbol and function mappings.
359 * Depending on the platform they either live in the background page or
360 * in the android math map.
361 */
362cvox.TraverseMath.prototype.initDomainsAndStyles = function() {
363  if (cvox.ChromeVox.host['mathMap']) {
364    this.addDomainsAndStyles(
365        cvox.ChromeVox.host['mathMap'].allDomains,
366        cvox.ChromeVox.host['mathMap'].allStyles);
367    } else {
368      cvox.ChromeVox.host.sendToBackgroundPage(
369          {'target': 'Math',
370           'action': 'getDomains'});
371    }
372};
373
374
375/**
376 * Sets the domain for the TraverseMath object to the next one in the list
377 * restarting from the first, if necessary.
378 * @return {string} The name of the newly set domain.
379 */
380cvox.TraverseMath.prototype.cycleDomain = function() {
381  this.initDomainsAndStyles();
382  var index = this.allDomains.indexOf(this.domain);
383  if (index == -1) {
384    return this.domain;
385  }
386  this.domain = this.allDomains[(++index) % this.allDomains.length];
387  return this.domain;
388};
389
390
391/**
392 * Sets the style for the TraverseMath object to the next one in the list
393 * restarting from the first, if necessary.
394 * @return {string} The name of the newly set style.
395 */
396cvox.TraverseMath.prototype.cycleStyle = function() {
397  this.initDomainsAndStyles();
398  var index = this.allStyles.indexOf(this.style);
399  if (index == -1) {
400    return this.domain;
401  }
402  this.style = this.allStyles[(++index) % this.allStyles.length];
403  return this.style;
404};
405
406
407/**
408 *  Sets the domain for the TraverseMath object.
409 * @param {string} domain Name of the domain.
410 * @private
411 */
412cvox.TraverseMath.prototype.setDomain_ = function(domain) {
413  if (this.allDomains.indexOf(domain) != -1) {
414    this.domain = domain;
415  } else {
416    this.domain = 'default';
417  }
418};
419
420
421/**
422 *  Sets the style for the TraverseMath object.
423 * @param {string} style Name of the style.
424 * @private
425 */
426cvox.TraverseMath.prototype.setStyle_ = function(style) {
427  if (this.allStyles.indexOf(style) != -1) {
428    this.style = style;
429  } else {
430    this.style = 'default';
431  }
432};
433
434
435/**
436 * Gets the active node attached to the current document.
437 * @return {Node} The active node, if it exists.
438 */
439cvox.TraverseMath.prototype.getAttachedActiveNode = function() {
440  var node = this.activeNode;
441  if (!node || node.nodeType != Node.ELEMENT_NODE) {
442    return null;
443  }
444  var id = node.getAttribute('spanID');
445  return document.getElementById(id);
446};
447