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 Implementation of the speech rule engine. 7 * 8 * The speech rule engine chooses and applies speech rules. Rules are chosen 9 * from a set of rule stores wrt. their applicability to a node in a particular 10 * markup type such as MathML or HTML. Rules are dispatched either by 11 * recursively computing new nodes and applicable rules or, if no further rule 12 * is applicable to a current node, by computing a speech object in the form of 13 * an array of navigation descriptions. 14 * 15 * Consequently the rule engine is parameterisable wrt. rule stores and 16 * evaluator function. 17 */ 18 19goog.provide('cvox.SpeechRuleEngine'); 20 21goog.require('cvox.BaseRuleStore'); 22goog.require('cvox.NavDescription'); 23goog.require('cvox.NavMathDescription'); 24goog.require('cvox.SpeechRule'); 25 26 27/** 28 * @constructor 29 */ 30cvox.SpeechRuleEngine = function() { 31 /** 32 * The currently active speech rule store. 33 * @type {cvox.BaseRuleStore} 34 * @private 35 */ 36 this.activeStore_ = null; 37 38 /** 39 * Dynamic constraint annotation. 40 * @type {!cvox.SpeechRule.DynamicCstr} 41 */ 42 this.dynamicCstr = {}; 43 this.dynamicCstr[cvox.SpeechRule.DynamicCstrAttrib.STYLE] = 'short'; 44}; 45goog.addSingletonGetter(cvox.SpeechRuleEngine); 46 47 48/** 49 * Parameterizes the speech rule engine. 50 * @param {cvox.BaseRuleStore} store A speech rule store. 51 */ 52cvox.SpeechRuleEngine.prototype.parameterize = function(store) { 53 try { 54 store.initialize(); 55 } catch (err) { 56 if (err.name == 'StoreError') { 57 console.log('Store Error:', err.message); 58 } 59 else { 60 throw err; 61 } 62 } 63 this.activeStore_ = store; 64}; 65 66 67/** 68 * Parameterizes the dynamic constraint annotation for the speech rule 69 * engine. This is a separate function as this can be done interactively, while 70 * a particular speech rule store is active. 71 * @param {cvox.SpeechRule.DynamicCstr} dynamic The new dynamic constraint. 72 */ 73cvox.SpeechRuleEngine.prototype.setDynamicConstraint = function(dynamic) { 74 if (dynamic) { 75 this.dynamicCstr = dynamic; 76 } 77}; 78 79 80/** 81 * Constructs a string from the node and the given expression. 82 * @param {!Node} node The initial node. 83 * @param {string} expr An Xpath expression string, a name of a custom 84 * function or a string. 85 * @return {string} The result of applying expression to node. 86 */ 87cvox.SpeechRuleEngine.prototype.constructString = function(node, expr) { 88 if (!expr) { 89 return ''; 90 } 91 if (expr.charAt(0) == '"') { 92 return expr.slice(1, -1); 93 } 94 var func = this.activeStore_.customStrings.lookup(expr); 95 if (func) { 96 // We always return the result of the custom function, in case it 97 // deliberately computes the empty string! 98 return func(node); 99 } 100 // Finally we assume expr to be an xpath expression and calculate a string 101 // value from the node. 102 return cvox.XpathUtil.evaluateString(expr, node); 103}; 104 105 106// Dispatch functionality. 107/** 108 * Computes a speech object for a given node. Returns the empty list if 109 * no node is given. 110 * @param {Node} node The node to be evaluated. 111 * @return {!Array.<cvox.NavDescription>} A list of navigation descriptions for 112 * that node. 113 */ 114cvox.SpeechRuleEngine.prototype.evaluateNode = function(node) { 115 if (!node) { 116 return []; 117 } 118 return this.evaluateTree_(node); 119}; 120 121 122/** 123 * Applies rules recursively to compute the final speech object. 124 * @param {!Node} node Node to apply the speech rule to. 125 * @return {!Array.<cvox.NavDescription>} A list of Navigation descriptions. 126 * @private 127 */ 128cvox.SpeechRuleEngine.prototype.evaluateTree_ = function(node) { 129 var rule = this.activeStore_.lookupRule(node, this.dynamicCstr); 130 if (!rule) { 131 return this.activeStore_.evaluateDefault(node); 132 } 133 var components = rule.action.components; 134 var result = []; 135 for (var i = 0, component; component = components[i]; i++) { 136 var navs = []; 137 var content = component['content'] || ''; 138 switch (component.type) { 139 case cvox.SpeechRule.Type.NODE: 140 var selected = this.activeStore_.applyQuery(node, content); 141 if (selected) { 142 navs = this.evaluateTree_(selected); 143 } 144 break; 145 case cvox.SpeechRule.Type.MULTI: 146 selected = this.activeStore_.applySelector(node, content); 147 if (selected.length > 0) { 148 navs = this.evaluateNodeList_( 149 selected, 150 component['sepFunc'], 151 this.constructString(node, component['separator']), 152 component['ctxtFunc'], 153 this.constructString(node, component['context'])); 154 } 155 break; 156 case cvox.SpeechRule.Type.TEXT: 157 selected = this.constructString(node, content); 158 if (selected) { 159 navs = [new cvox.NavDescription({text: selected})]; 160 } 161 break; 162 case cvox.SpeechRule.Type.PERSONALITY: 163 default: 164 navs = [new cvox.NavDescription({text: content})]; 165 } 166 // Adding overall context if it exists. 167 if (navs[0] && component['context'] && 168 component.type != cvox.SpeechRule.Type.MULTI) { 169 navs[0]['context'] = 170 this.constructString(node, component['context']) + 171 (navs[0]['context'] || ''); 172 } 173 // Adding personality to the nav descriptions. 174 result = result.concat(this.addPersonality_(navs, component)); 175 } 176 return result; 177}; 178 179 180/** 181 * Evaluates a list of nodes into a list of navigation descriptions. 182 * @param {!Array.<Node>} nodes Array of nodes. 183 * @param {string} sepFunc Name of a function used to compute a separator 184 * between every element. 185 * @param {string} separator A string that is used as argument to the sepFunc or 186 * interspersed directly between each node if sepFunc is not supplied. 187 * @param {string} ctxtFunc Name of a function applied to compute the context 188 * for every element in the list. 189 * @param {string} context Additional context string that is given to the 190 * ctxtFunc function or used directly if ctxtFunc is not supplied. 191 * @return {Array.<cvox.NavDescription>} A list of Navigation descriptions. 192 * @private 193 */ 194cvox.SpeechRuleEngine.prototype.evaluateNodeList_ = function( 195 nodes, sepFunc, separator, ctxtFunc, context) { 196 if (nodes == []) { 197 return []; 198 } 199 var sep = separator || ''; 200 var cont = context || ''; 201 var cFunc = this.activeStore_.contextFunctions.lookup(ctxtFunc); 202 var ctxtClosure = cFunc ? cFunc(nodes, cont) : function() {return cont;}; 203 var sFunc = this.activeStore_.contextFunctions.lookup(sepFunc); 204 var sepClosure = sFunc ? sFunc(nodes, sep) : function() {return sep;}; 205 var result = []; 206 for (var i = 0, node; node = nodes[i]; i++) { 207 var navs = this.evaluateTree_(node); 208 if (navs.length > 0) { 209 navs[0]['context'] = ctxtClosure() + (navs[0]['context'] || ''); 210 result = result.concat(navs); 211 if (i < nodes.length - 1) { 212 var text = sepClosure(); 213 if (text) { 214 result.push(new cvox.NavDescription({text: text})); 215 } 216 } 217 } 218 } 219 return result; 220}; 221 222 223/** 224 * Maps properties in speech rules to personality properties. 225 * @type {{pitch : string, 226 * rate: string, 227 * volume: string, 228 * pause: string}} 229 * @const 230 */ 231cvox.SpeechRuleEngine.propMap = {'pitch': cvox.AbstractTts.RELATIVE_PITCH, 232 'rate': cvox.AbstractTts.RELATIVE_RATE, 233 'volume': cvox.AbstractTts.RELATIVE_VOLUME, 234 'pause': cvox.AbstractTts.PAUSE 235 }; 236 237 238/** 239 * Adds personality to every Navigation Descriptions in input list. 240 * @param {Array.<cvox.NavDescription>} navs A list of Navigation descriptions. 241 * @param {Object} props Property dictionary. 242 * TODO (sorge) Fully specify, when we have finalised the speech rule 243 * format. 244 * @return {Array.<cvox.NavDescription>} The modified array. 245 * @private 246 */ 247cvox.SpeechRuleEngine.prototype.addPersonality_ = function(navs, props) { 248 var personality = {}; 249 for (var key in cvox.SpeechRuleEngine.propMap) { 250 var value = parseFloat(props[key]); 251 if (!isNaN(value)) { 252 personality[cvox.SpeechRuleEngine.propMap[key]] = value; 253 } 254 } 255 navs.forEach(goog.bind(function(nav) { 256 this.addRelativePersonality_(nav, personality); 257 this.resetPersonality_(nav); 258 }, this)); 259 return navs; 260}; 261 262 263/** 264 * Adds relative personality entries to the personality of a Navigation 265 * Description. 266 * @param {cvox.NavDescription|cvox.NavMathDescription} nav Nav Description. 267 * @param {!Object} personality Dictionary with relative personality entries. 268 * @return {cvox.NavDescription|cvox.NavMathDescription} Updated description. 269 * @private 270 */ 271cvox.SpeechRuleEngine.prototype.addRelativePersonality_ = function( 272 nav, personality) { 273 if (!nav['personality']) { 274 nav['personality'] = personality; 275 return nav; 276 } 277 var navPersonality = nav['personality']; 278 for (var p in personality) { 279 // Although values could exceed boundaries, they will be limited to the 280 // correct interval via the call to 281 // cvox.AbstractTts.prototype.mergeProperties in 282 // cvox.TtsBackground.prototype.speak 283 if (navPersonality[p] && typeof(navPersonality[p]) == 'number') { 284 navPersonality[p] = navPersonality[p] + personality[p]; 285 } else { 286 navPersonality[p] = personality[p]; 287 } 288 } 289 return nav; 290}; 291 292 293/** 294 * Resets personalities to default values if necessary. 295 * @param {cvox.NavDescription|cvox.NavMathDescription} nav Nav Description. 296 * @private 297 */ 298cvox.SpeechRuleEngine.prototype.resetPersonality_ = function(nav) { 299 if (this.activeStore_.defaultTtsProps) { 300 for (var i = 0, prop; prop = this.activeStore_.defaultTtsProps[i]; i++) { 301 nav.personality[prop] = cvox.ChromeVox.tts.getDefaultProperty(prop); 302 } 303 } 304}; 305 306 307/** 308 * Flag for the debug mode of the speech rule engine. 309 * @type {boolean} 310 */ 311cvox.SpeechRuleEngine.debugMode = false; 312 313 314/** 315 * Give debug output. 316 * @param {...*} output Rest elements of debug output. 317 */ 318cvox.SpeechRuleEngine.outputDebug = function(output) { 319 if (cvox.SpeechRuleEngine.debugMode) { 320 var outputList = Array.prototype.slice.call(arguments, 0); 321 console.log.apply(console, 322 ['Speech Rule Engine Debugger:'].concat(outputList)); 323 } 324}; 325 326 327/** 328 * Prints the list of all current rules in ChromeVox to the console. 329 * @return {string} A textual representation of all rules in the speech rule 330 * engine. 331 */ 332cvox.SpeechRuleEngine.prototype.toString = function() { 333 var allRules = this.activeStore_.findAllRules(function(x) {return true;}); 334 return allRules.map(function(rule) {return rule.toString();}). 335 join('\n'); 336}; 337 338 339/** 340 * Test the precondition of a speech rule in debugging mode. 341 * @param {cvox.SpeechRule} rule A speech rule. 342 * @param {!Node} node DOM node to test applicability of the rule. 343 */ 344cvox.SpeechRuleEngine.debugSpeechRule = function(rule, node) { 345 var store = cvox.SpeechRuleEngine.getInstance().activeStore_; 346 if (store) { 347 var prec = rule.precondition; 348 cvox.SpeechRuleEngine.outputDebug( 349 prec.query, store.applyQuery(node, prec.query)); 350 prec.constraints.forEach( 351 function(cstr) { 352 cvox.SpeechRuleEngine.outputDebug( 353 cstr, store.applyConstraint(node, cstr));}); 354 } 355}; 356 357 358/** 359 * Test the precondition of a speech rule in debugging mode. 360 * @param {string} name Rule to debug. 361 * @param {!Node} node DOM node to test applicability of the rule. 362 */ 363cvox.SpeechRuleEngine.debugNamedSpeechRule = function(name, node) { 364 var store = cvox.SpeechRuleEngine.getInstance().activeStore_; 365 var allRules = store.findAllRules( 366 function(rule) {return rule.name == name;}); 367 for (var i = 0, rule; rule = allRules[i]; i++) { 368 cvox.SpeechRuleEngine.outputDebug('Rule', name, 'number', i); 369 cvox.SpeechRuleEngine.debugSpeechRule(rule, node); 370 } 371}; 372