Home | History | Annotate | Download | only in speech_rules
      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