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