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