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 Base class for all speech rule stores. 7 * 8 * The base rule store implements some basic functionality that is common to 9 * most speech rule stores. 10 */ 11 12 goog.provide('cvox.BaseRuleStore'); 13 14 goog.require('cvox.MathUtil'); 15 goog.require('cvox.SpeechRule'); 16 goog.require('cvox.SpeechRuleEvaluator'); 17 goog.require('cvox.SpeechRuleFunctions'); 18 goog.require('cvox.SpeechRuleStore'); 19 20 21 /** 22 * @constructor 23 * @implements {cvox.SpeechRuleEvaluator} 24 * @implements {cvox.SpeechRuleStore} 25 */ 26 cvox.BaseRuleStore = function() { 27 /** 28 * Set of custom query functions for the store. 29 * @type {cvox.SpeechRuleFunctions.CustomQueries} 30 */ 31 this.customQueries = new cvox.SpeechRuleFunctions.CustomQueries(); 32 33 /** 34 * Set of custom strings for the store. 35 * @type {cvox.SpeechRuleFunctions.CustomStrings} 36 */ 37 this.customStrings = new cvox.SpeechRuleFunctions.CustomStrings(); 38 39 /** 40 * Set of context functions for the store. 41 * @type {cvox.SpeechRuleFunctions.ContextFunctions} 42 */ 43 this.contextFunctions = new cvox.SpeechRuleFunctions.ContextFunctions(); 44 45 /** 46 * Set of speech rules in the store. 47 * @type {!Array.<cvox.SpeechRule>} 48 * @private 49 */ 50 this.speechRules_ = []; 51 52 /** 53 * A priority list of dynamic constraint attributes. 54 * @type {!Array.<cvox.SpeechRule.DynamicCstrAttrib>} 55 */ 56 this.dynamicCstrAttribs = [cvox.SpeechRule.DynamicCstrAttrib.STYLE]; 57 58 /** 59 * List of TTS properties overridden by the store when it is active. 60 * @type {!Array.<string>} 61 */ 62 this.defaultTtsProps = []; 63 }; 64 65 66 /** 67 * @override 68 */ 69 cvox.BaseRuleStore.prototype.lookupRule = function(node, dynamic) { 70 if (!node || 71 (node.nodeType != Node.ELEMENT_NODE && node.nodeType != Node.TEXT_NODE)) { 72 return null; 73 } 74 var matchingRules = this.speechRules_.filter( 75 goog.bind( 76 function(rule) { 77 return this.testDynamicConstraints(dynamic, rule) && 78 this.testPrecondition_(/** @type {!Node} */ (node), rule);}, 79 this)); 80 return (matchingRules.length > 0) ? 81 this.pickMostConstraint_(dynamic, matchingRules) : null; 82 }; 83 84 85 /** 86 * @override 87 */ 88 cvox.BaseRuleStore.prototype.defineRule = function( 89 name, dynamic, action, prec, cstr) { 90 try { 91 var postc = cvox.SpeechRule.Action.fromString(action); 92 var cstrList = Array.prototype.slice.call(arguments, 4); 93 var fullPrec = new cvox.SpeechRule.Precondition(prec, cstrList); 94 var dynamicCstr = {}; 95 dynamicCstr[cvox.SpeechRule.DynamicCstrAttrib.STYLE] = dynamic; 96 var rule = new cvox.SpeechRule(name, dynamicCstr, fullPrec, postc); 97 } catch (err) { 98 if (err.name == 'RuleError') { 99 console.log('Rule Error ', prec, '(' + dynamic + '):', err.message); 100 return null; 101 } 102 else { 103 throw err; 104 } 105 } 106 this.addRule(rule); 107 return rule; 108 }; 109 110 111 /** 112 * @override 113 */ 114 cvox.BaseRuleStore.prototype.addRule = function(rule) { 115 this.speechRules_.unshift(rule); 116 }; 117 118 119 /** 120 * @override 121 */ 122 cvox.BaseRuleStore.prototype.deleteRule = function(rule) { 123 var index = this.speechRules_.indexOf(rule); 124 if (index != -1) { 125 this.speechRules_.splice(index, 1); 126 } 127 }; 128 129 130 /** 131 * @override 132 */ 133 cvox.BaseRuleStore.prototype.findRule = function(pred) { 134 for (var i = 0, rule; rule = this.speechRules_[i]; i++) { 135 if (pred(rule)) { 136 return rule; 137 } 138 } 139 return null; 140 }; 141 142 143 /** 144 * @override 145 */ 146 cvox.BaseRuleStore.prototype.findAllRules = function(pred) { 147 return this.speechRules_.filter(pred); 148 }; 149 150 151 /** 152 * @override 153 */ 154 cvox.BaseRuleStore.prototype.evaluateDefault = function(node) { 155 return [new cvox.NavDescription({'text': node.textContent})]; 156 }; 157 158 159 /** 160 * Test the applicability of a speech rule in debugging mode. 161 * @param {string} name Rule to debug. 162 * @param {!Node} node DOM node to test applicability of given rule. 163 */ 164 cvox.BaseRuleStore.prototype.debugSpeechRule = goog.abstractMethod; 165 166 167 /** 168 * Function to initialize the store with speech rules. It is called by the 169 * speech rule engine upon parametrization with this store. The function allows 170 * us to define sets of rules in separate files while depending on functionality 171 * that is defined in the rule store. 172 * Essentially it is a way of getting around dependencies. 173 */ 174 cvox.BaseRuleStore.prototype.initialize = goog.abstractMethod; 175 176 177 /** 178 * Removes duplicates of the given rule from the rule store. Thereby duplicates 179 * are identified by having the same precondition and dynamic constraint. 180 * @param {cvox.SpeechRule} rule The rule. 181 */ 182 cvox.BaseRuleStore.prototype.removeDuplicates = function(rule) { 183 for (var i = this.speechRules_.length - 1, oldRule; 184 oldRule = this.speechRules_[i]; i--) { 185 if (oldRule != rule && 186 cvox.BaseRuleStore.compareDynamicConstraints_( 187 oldRule.dynamicCstr, rule.dynamicCstr) && 188 cvox.BaseRuleStore.comparePreconditions_(oldRule, rule)) { 189 this.speechRules_.splice(i, 1); 190 } 191 } 192 }; 193 194 195 // TODO (sorge) These should move into the speech rule functions. 196 /** 197 * Checks if we have a custom query and applies it. Otherwise returns null. 198 * @param {!Node} node The initial node. 199 * @param {string} funcName A function name. 200 * @return {Array.<Node>} The list of resulting nodes. 201 */ 202 cvox.BaseRuleStore.prototype.applyCustomQuery = function( 203 node, funcName) { 204 var func = this.customQueries.lookup(funcName); 205 return func ? func(node) : null; 206 }; 207 208 209 /** 210 * Applies either an Xpath selector or a custom query to the node 211 * and returns the resulting node list. 212 * @param {!Node} node The initial node. 213 * @param {string} expr An Xpath expression string or a name of a custom 214 * query. 215 * @return {Array.<Node>} The list of resulting nodes. 216 */ 217 cvox.BaseRuleStore.prototype.applySelector = function(node, expr) { 218 var result = this.applyCustomQuery(node, expr); 219 return result || cvox.XpathUtil.evalXPath(expr, node); 220 }; 221 222 223 /** 224 * Applies either an Xpath selector or a custom query to the node 225 * and returns the first result. 226 * @param {!Node} node The initial node. 227 * @param {string} expr An Xpath expression string or a name of a custom 228 * query. 229 * @return {Node} The resulting node. 230 */ 231 cvox.BaseRuleStore.prototype.applyQuery = function(node, expr) { 232 var results = this.applySelector(node, expr); 233 if (results.length > 0) { 234 return results[0]; 235 } 236 return null; 237 }; 238 239 240 /** 241 * Applies either an Xpath selector or a custom query to the node and returns 242 * true if the application yields a non-empty result. 243 * @param {!Node} node The initial node. 244 * @param {string} expr An Xpath expression string or a name of a custom 245 * query. 246 * @return {boolean} True if application was successful. 247 */ 248 cvox.BaseRuleStore.prototype.applyConstraint = function(node, expr) { 249 var result = this.applyQuery(node, expr); 250 return !!result || cvox.XpathUtil.evaluateBoolean(expr, node); 251 }; 252 253 254 /** 255 * Tests whether a speech rule satisfies a set of dynamic constraints. 256 * @param {!cvox.SpeechRule.DynamicCstr} dynamic Dynamic constraints. 257 * @param {cvox.SpeechRule} rule The rule. 258 * @return {boolean} True if the preconditions apply to the node. 259 * @protected 260 */ 261 cvox.BaseRuleStore.prototype.testDynamicConstraints = function( 262 dynamic, rule) { 263 // We allow a default value for each dynamic constraints attribute. 264 // The idea is that when we can not find a speech rule matching the value for 265 // a particular attribute in the dynamic constraintwe choose the one that has 266 // the value 'default'. 267 var allKeys = /** @type {Array.<cvox.SpeechRule.DynamicCstrAttrib>} */ ( 268 Object.keys(dynamic)); 269 return allKeys.every( 270 function(key) { 271 return dynamic[key] == rule.dynamicCstr[key] || 272 rule.dynamicCstr[key] == 'default'; 273 }); 274 }; 275 276 277 /** 278 * Get a set of all dynamic constraint values. 279 * @return {!Object.<cvox.SpeechRule.DynamicCstrAttrib, Array.<string>>} The 280 * object with all annotations. 281 */ 282 cvox.BaseRuleStore.prototype.getDynamicConstraintValues = function() { 283 var result = {}; 284 for (var i = 0, rule; rule = this.speechRules_[i]; i++) { 285 for (var key in rule.dynamicCstr) { 286 var newKey = [rule.dynamicCstr[key]]; 287 if (result[key]) { 288 result[key] = cvox.MathUtil.union(result[key], newKey); 289 } else { 290 result[key] = newKey; 291 } 292 } 293 } 294 return result; 295 }; 296 297 298 /** 299 * Counts how many dynamic constraint values match exactly in the order 300 * specified by the store. 301 * @param {cvox.SpeechRule.DynamicCstr} dynamic Dynamic constraints. 302 * @param {cvox.SpeechRule} rule The speech rule to match. 303 * @return {number} The number of matching dynamic constraint values. 304 * @private 305 */ 306 cvox.BaseRuleStore.prototype.countMatchingDynamicConstraintValues_ = function( 307 dynamic, rule) { 308 var result = 0; 309 for (var i = 0, key; key = this.dynamicCstrAttribs[i]; i++) { 310 if (dynamic[key] == rule.dynamicCstr[key]) { 311 result++; 312 } else break; 313 } 314 return result; 315 }; 316 317 318 /** 319 * Picks the result of the most constraint rule by prefering those: 320 * 1) that best match the dynamic constraints. 321 * 2) with the most additional constraints. 322 * @param {cvox.SpeechRule.DynamicCstr} dynamic Dynamic constraints. 323 * @param {!Array.<cvox.SpeechRule>} rules An array of rules. 324 * @return {cvox.SpeechRule} The most constraint rule. 325 * @private 326 */ 327 cvox.BaseRuleStore.prototype.pickMostConstraint_ = function(dynamic, rules) { 328 rules.sort(goog.bind( 329 function(r1, r2) { 330 var count1 = this.countMatchingDynamicConstraintValues_(dynamic, r1); 331 var count2 = this.countMatchingDynamicConstraintValues_(dynamic, r2); 332 // Rule one is a better match, don't swap. 333 if (count1 > count2) { 334 return -1; 335 } 336 // Rule two is a better match, swap. 337 if (count2 > count1) { 338 return 1; 339 } 340 // When same number of dynamic constraint attributes matches for 341 // both rules, compare length of static constraints. 342 return (r2.precondition.constraints.length - 343 r1.precondition.constraints.length);}, 344 this)); 345 return rules[0]; 346 }; 347 348 349 /** 350 * Test the precondition of a speech rule. 351 * @param {!Node} node on which to test applicability of the rule. 352 * @param {cvox.SpeechRule} rule The rule to be tested. 353 * @return {boolean} True if the preconditions apply to the node. 354 * @private 355 */ 356 cvox.BaseRuleStore.prototype.testPrecondition_ = function(node, rule) { 357 var prec = rule.precondition; 358 return this.applyQuery(node, prec.query) === node && 359 prec.constraints.every( 360 goog.bind(function(cstr) { 361 return this.applyConstraint(node, cstr);}, 362 this)); 363 }; 364 365 366 // TODO (sorge) Define the following methods directly on the dynamic constraint 367 // and precondition classes, respectively. 368 /** 369 * Compares two dynamic constraints and returns true if they are equal. 370 * @param {cvox.SpeechRule.DynamicCstr} cstr1 First dynamic constraints. 371 * @param {cvox.SpeechRule.DynamicCstr} cstr2 Second dynamic constraints. 372 * @return {boolean} True if the dynamic constraints are equal. 373 * @private 374 */ 375 cvox.BaseRuleStore.compareDynamicConstraints_ = function( 376 cstr1, cstr2) { 377 if (Object.keys(cstr1).length != Object.keys(cstr2).length) { 378 return false; 379 } 380 for (var key in cstr1) { 381 if (!cstr2[key] || cstr1[key] !== cstr2[key]) { 382 return false; 383 } 384 } 385 return true; 386 }; 387 388 389 /** 390 * Compares two static constraints (i.e., lists of precondition constraints) and 391 * returns true if they are equal. 392 * @param {Array.<string>} cstr1 First static constraints. 393 * @param {Array.<string>} cstr2 Second static constraints. 394 * @return {boolean} True if the static constraints are equal. 395 * @private 396 */ 397 cvox.BaseRuleStore.compareStaticConstraints_ = function( 398 cstr1, cstr2) { 399 if (cstr1.length != cstr2.length) { 400 return false; 401 } 402 for (var i = 0, cstr; cstr = cstr1[i]; i++) { 403 if (cstr2.indexOf(cstr) == -1) { 404 return false; 405 } 406 } 407 return true; 408 }; 409 410 411 /** 412 * Compares the preconditions of two speech rules. 413 * @param {cvox.SpeechRule} rule1 The first speech rule. 414 * @param {cvox.SpeechRule} rule2 The second speech rule. 415 * @return {boolean} True if the preconditions are equal. 416 * @private 417 */ 418 cvox.BaseRuleStore.comparePreconditions_ = function(rule1, rule2) { 419 var prec1 = rule1.precondition; 420 var prec2 = rule2.precondition; 421 if (prec1.query != prec2.query) { 422 return false; 423 } 424 return cvox.BaseRuleStore.compareStaticConstraints_( 425 prec1.constraints, prec2.constraints); 426 }; 427