base_rule_store.js revision cedac228d2dd51db4b79ea1e72c7f249408ee061
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
12goog.provide('cvox.BaseRuleStore');
13
14goog.require('cvox.MathUtil');
15goog.require('cvox.SpeechRule');
16goog.require('cvox.SpeechRuleEvaluator');
17goog.require('cvox.SpeechRuleFunctions');
18goog.require('cvox.SpeechRuleStore');
19
20
21/**
22 * @constructor
23 * @implements {cvox.SpeechRuleEvaluator}
24 * @implements {cvox.SpeechRuleStore}
25 */
26cvox.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 */
69cvox.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 */
88cvox.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 */
114cvox.BaseRuleStore.prototype.addRule = function(rule) {
115  this.speechRules_.unshift(rule);
116};
117
118
119/**
120 * @override
121 */
122cvox.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 */
133cvox.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 */
146cvox.BaseRuleStore.prototype.findAllRules = function(pred) {
147  return this.speechRules_.filter(pred);
148};
149
150
151/**
152 * @override
153 */
154cvox.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 */
164cvox.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 */
174cvox.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 */
182cvox.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 */
202cvox.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 */
217cvox.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 */
231cvox.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 */
248cvox.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 */
261cvox.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 */
282cvox.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 */
306cvox.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 */
327cvox.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 */
356cvox.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 */
375cvox.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 */
397cvox.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 */
418cvox.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