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.
6 * @fileoverview Implementation of the speech rule engine.
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.
15 * Consequently the rule engine is parameterisable wrt. rule stores and
19 goog.provide('cvox.SpeechRuleEngine');
21 goog.require('cvox.BaseRuleStore');
22 goog.require('cvox.NavDescription');
23 goog.require('cvox.NavMathDescription');
24 goog.require('cvox.SpeechRule');
30 cvox.SpeechRuleEngine = function() {
32 * The currently active speech rule store.
33 * @type {cvox.BaseRuleStore}
36 this.activeStore_ = null;
39 * Dynamic constraint annotation.
40 * @type {!cvox.SpeechRule.DynamicCstr}
42 this.dynamicCstr = {};
43 this.dynamicCstr[cvox.SpeechRule.DynamicCstrAttrib.STYLE] = 'short';
45 goog.addSingletonGetter(cvox.SpeechRuleEngine);
49 * Parameterizes the speech rule engine.
50 * @param {cvox.BaseRuleStore} store A speech rule store.
52 cvox.SpeechRuleEngine.prototype.parameterize = function(store) {
56 if (err.name == 'StoreError') {
57 console.log('Store Error:', err.message);
63 this.activeStore_ = store;
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.
73 cvox.SpeechRuleEngine.prototype.setDynamicConstraint = function(dynamic) {
75 this.dynamicCstr = dynamic;
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.
87 cvox.SpeechRuleEngine.prototype.constructString = function(node, expr) {
91 if (expr.charAt(0) == '"') {
92 return expr.slice(1, -1);
94 var func = this.activeStore_.customStrings.lookup(expr);
96 // We always return the result of the custom function, in case it
97 // deliberately computes the empty string!
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);
106 // Dispatch functionality.
108 * Computes a speech object for a given node. Returns the empty list if
110 * @param {Node} node The node to be evaluated.
111 * @return {!Array<cvox.NavDescription>} A list of navigation descriptions for
114 cvox.SpeechRuleEngine.prototype.evaluateNode = function(node) {
118 return this.evaluateTree_(node);
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.
128 cvox.SpeechRuleEngine.prototype.evaluateTree_ = function(node) {
129 var rule = this.activeStore_.lookupRule(node, this.dynamicCstr);
131 return this.activeStore_.evaluateDefault(node);
133 var components = rule.action.components;
135 for (var i = 0, component; component = components[i]; i++) {
137 var content = component['content'] || '';
138 switch (component.type) {
139 case cvox.SpeechRule.Type.NODE:
140 var selected = this.activeStore_.applyQuery(node, content);
142 navs = this.evaluateTree_(selected);
145 case cvox.SpeechRule.Type.MULTI:
146 selected = this.activeStore_.applySelector(node, content);
147 if (selected.length > 0) {
148 navs = this.evaluateNodeList_(
150 component['sepFunc'],
151 this.constructString(node, component['separator']),
152 component['ctxtFunc'],
153 this.constructString(node, component['context']));
156 case cvox.SpeechRule.Type.TEXT:
157 selected = this.constructString(node, content);
159 navs = [new cvox.NavDescription({text: selected})];
162 case cvox.SpeechRule.Type.PERSONALITY:
164 navs = [new cvox.NavDescription({text: content})];
166 // Adding overall context if it exists.
167 if (navs[0] && component['context'] &&
168 component.type != cvox.SpeechRule.Type.MULTI) {
170 this.constructString(node, component['context']) +
171 (navs[0]['context'] || '');
173 // Adding personality to the nav descriptions.
174 result = result.concat(this.addPersonality_(navs, component));
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.
194 cvox.SpeechRuleEngine.prototype.evaluateNodeList_ = function(
195 nodes, sepFunc, separator, ctxtFunc, context) {
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;};
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();
214 result.push(new cvox.NavDescription({text: text}));
224 * Maps properties in speech rules to personality properties.
225 * @type {{pitch : string,
231 cvox.SpeechRuleEngine.propMap = {'pitch': cvox.AbstractTts.RELATIVE_PITCH,
232 'rate': cvox.AbstractTts.RELATIVE_RATE,
233 'volume': cvox.AbstractTts.RELATIVE_VOLUME,
234 'pause': cvox.AbstractTts.PAUSE
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
244 * @return {Array<cvox.NavDescription>} The modified array.
247 cvox.SpeechRuleEngine.prototype.addPersonality_ = function(navs, props) {
248 var personality = {};
249 for (var key in cvox.SpeechRuleEngine.propMap) {
250 var value = parseFloat(props[key]);
252 personality[cvox.SpeechRuleEngine.propMap[key]] = value;
255 navs.forEach(goog.bind(function(nav) {
256 this.addRelativePersonality_(nav, personality);
257 this.resetPersonality_(nav);
264 * Adds relative personality entries to the personality of a Navigation
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.
271 cvox.SpeechRuleEngine.prototype.addRelativePersonality_ = function(
273 if (!nav['personality']) {
274 nav['personality'] = personality;
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];
286 navPersonality[p] = personality[p];
294 * Resets personalities to default values if necessary.
295 * @param {cvox.NavDescription|cvox.NavMathDescription} nav Nav Description.
298 cvox.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);
308 * Flag for the debug mode of the speech rule engine.
311 cvox.SpeechRuleEngine.debugMode = false;
316 * @param {...*} output Rest elements of debug output.
318 cvox.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));
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
332 cvox.SpeechRuleEngine.prototype.toString = function() {
333 var allRules = this.activeStore_.findAllRules(function(x) {return true;});
334 return allRules.map(function(rule) {return rule.toString();}).
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.
344 cvox.SpeechRuleEngine.debugSpeechRule = function(rule, node) {
345 var store = cvox.SpeechRuleEngine.getInstance().activeStore_;
347 var prec = rule.precondition;
348 cvox.SpeechRuleEngine.outputDebug(
349 prec.query, store.applyQuery(node, prec.query));
350 prec.constraints.forEach(
352 cvox.SpeechRuleEngine.outputDebug(
353 cstr, store.applyConstraint(node, cstr));});
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.
363 cvox.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);