blob: 39bc8ac0adf3c690bede37ef2a8c6eb7f7a3b55c [file] [log] [blame]
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* @fileoverview Implementation of the speech rule engine.
*
* The speech rule engine chooses and applies speech rules. Rules are chosen
* from a set of rule stores wrt. their applicability to a node in a particular
* markup type such as MathML or HTML. Rules are dispatched either by
* recursively computing new nodes and applicable rules or, if no further rule
* is applicable to a current node, by computing a speech object in the form of
* an array of navigation descriptions.
*
* Consequently the rule engine is parameterisable wrt. rule stores and
* evaluator function.
*/
goog.provide('cvox.SpeechRuleEngine');
goog.require('cvox.BaseRuleStore');
goog.require('cvox.NavDescription');
goog.require('cvox.NavMathDescription');
goog.require('cvox.SpeechRule');
/**
* @constructor
*/
cvox.SpeechRuleEngine = function() {
/**
* The currently active speech rule store.
* @type {cvox.BaseRuleStore}
* @private
*/
this.activeStore_ = null;
/**
* Dynamic constraint annotation.
* @type {!cvox.SpeechRule.DynamicCstr}
*/
this.dynamicCstr = {};
this.dynamicCstr[cvox.SpeechRule.DynamicCstrAttrib.STYLE] = 'short';
};
goog.addSingletonGetter(cvox.SpeechRuleEngine);
/**
* Parameterizes the speech rule engine.
* @param {cvox.BaseRuleStore} store A speech rule store.
*/
cvox.SpeechRuleEngine.prototype.parameterize = function(store) {
try {
store.initialize();
} catch (err) {
if (err.name == 'StoreError') {
console.log('Store Error:', err.message);
}
else {
throw err;
}
}
this.activeStore_ = store;
};
/**
* Parameterizes the dynamic constraint annotation for the speech rule
* engine. This is a separate function as this can be done interactively, while
* a particular speech rule store is active.
* @param {cvox.SpeechRule.DynamicCstr} dynamic The new dynamic constraint.
*/
cvox.SpeechRuleEngine.prototype.setDynamicConstraint = function(dynamic) {
if (dynamic) {
this.dynamicCstr = dynamic;
}
};
/**
* Constructs a string from the node and the given expression.
* @param {!Node} node The initial node.
* @param {string} expr An Xpath expression string, a name of a custom
* function or a string.
* @return {string} The result of applying expression to node.
*/
cvox.SpeechRuleEngine.prototype.constructString = function(node, expr) {
if (!expr) {
return '';
}
if (expr.charAt(0) == '"') {
return expr.slice(1, -1);
}
var func = this.activeStore_.customStrings.lookup(expr);
if (func) {
// We always return the result of the custom function, in case it
// deliberately computes the empty string!
return func(node);
}
// Finally we assume expr to be an xpath expression and calculate a string
// value from the node.
return cvox.XpathUtil.evaluateString(expr, node);
};
// Dispatch functionality.
/**
* Computes a speech object for a given node. Returns the empty list if
* no node is given.
* @param {Node} node The node to be evaluated.
* @return {!Array.<cvox.NavDescription>} A list of navigation descriptions for
* that node.
*/
cvox.SpeechRuleEngine.prototype.evaluateNode = function(node) {
if (!node) {
return [];
}
return this.evaluateTree_(node);
};
/**
* Applies rules recursively to compute the final speech object.
* @param {!Node} node Node to apply the speech rule to.
* @return {!Array.<cvox.NavDescription>} A list of Navigation descriptions.
* @private
*/
cvox.SpeechRuleEngine.prototype.evaluateTree_ = function(node) {
var rule = this.activeStore_.lookupRule(node, this.dynamicCstr);
if (!rule) {
return this.activeStore_.evaluateDefault(node);
}
var components = rule.action.components;
var result = [];
for (var i = 0, component; component = components[i]; i++) {
var navs = [];
var content = component['content'] || '';
switch (component.type) {
case cvox.SpeechRule.Type.NODE:
var selected = this.activeStore_.applyQuery(node, content);
if (selected) {
navs = this.evaluateTree_(selected);
}
break;
case cvox.SpeechRule.Type.MULTI:
selected = this.activeStore_.applySelector(node, content);
if (selected.length > 0) {
navs = this.evaluateNodeList_(
selected,
component['sepFunc'],
this.constructString(node, component['separator']),
component['ctxtFunc'],
this.constructString(node, component['context']));
}
break;
case cvox.SpeechRule.Type.TEXT:
selected = this.constructString(node, content);
if (selected) {
navs = [new cvox.NavDescription({text: selected})];
}
break;
case cvox.SpeechRule.Type.PERSONALITY:
default:
navs = [new cvox.NavDescription({text: content})];
}
// Adding overall context if it exists.
if (navs[0] && component['context'] &&
component.type != cvox.SpeechRule.Type.MULTI) {
navs[0]['context'] =
this.constructString(node, component['context']) +
(navs[0]['context'] || '');
}
// Adding personality to the nav descriptions.
result = result.concat(this.addPersonality_(navs, component));
}
return result;
};
/**
* Evaluates a list of nodes into a list of navigation descriptions.
* @param {!Array.<Node>} nodes Array of nodes.
* @param {string} sepFunc Name of a function used to compute a separator
* between every element.
* @param {string} separator A string that is used as argument to the sepFunc or
* interspersed directly between each node if sepFunc is not supplied.
* @param {string} ctxtFunc Name of a function applied to compute the context
* for every element in the list.
* @param {string} context Additional context string that is given to the
* ctxtFunc function or used directly if ctxtFunc is not supplied.
* @return {Array.<cvox.NavDescription>} A list of Navigation descriptions.
* @private
*/
cvox.SpeechRuleEngine.prototype.evaluateNodeList_ = function(
nodes, sepFunc, separator, ctxtFunc, context) {
if (nodes == []) {
return [];
}
var sep = separator || '';
var cont = context || '';
var cFunc = this.activeStore_.contextFunctions.lookup(ctxtFunc);
var ctxtClosure = cFunc ? cFunc(nodes, cont) : function() {return cont;};
var sFunc = this.activeStore_.contextFunctions.lookup(sepFunc);
var sepClosure = sFunc ? sFunc(nodes, sep) : function() {return sep;};
var result = [];
for (var i = 0, node; node = nodes[i]; i++) {
var navs = this.evaluateTree_(node);
if (navs.length > 0) {
navs[0]['context'] = ctxtClosure() + (navs[0]['context'] || '');
result = result.concat(navs);
if (i < nodes.length - 1) {
var text = sepClosure();
if (text) {
result.push(new cvox.NavDescription({text: text}));
}
}
}
}
return result;
};
/**
* Maps properties in speech rules to personality properties.
* @type {{pitch : string,
* rate: string,
* volume: string,
* pause: string}}
* @const
*/
cvox.SpeechRuleEngine.propMap = {'pitch': cvox.AbstractTts.RELATIVE_PITCH,
'rate': cvox.AbstractTts.RELATIVE_RATE,
'volume': cvox.AbstractTts.RELATIVE_VOLUME,
'pause': cvox.AbstractTts.PAUSE
};
/**
* Adds personality to every Navigation Descriptions in input list.
* @param {Array.<cvox.NavDescription>} navs A list of Navigation descriptions.
* @param {Object} props Property dictionary.
* TODO (sorge) Fully specify, when we have finalised the speech rule
* format.
* @return {Array.<cvox.NavDescription>} The modified array.
* @private
*/
cvox.SpeechRuleEngine.prototype.addPersonality_ = function(navs, props) {
var personality = {};
for (var key in cvox.SpeechRuleEngine.propMap) {
var value = parseFloat(props[key]);
if (!isNaN(value)) {
personality[cvox.SpeechRuleEngine.propMap[key]] = value;
}
}
navs.forEach(goog.bind(function(nav) {
this.addRelativePersonality_(nav, personality);
this.resetPersonality_(nav);
}, this));
return navs;
};
/**
* Adds relative personality entries to the personality of a Navigation
* Description.
* @param {cvox.NavDescription|cvox.NavMathDescription} nav Nav Description.
* @param {!Object} personality Dictionary with relative personality entries.
* @return {cvox.NavDescription|cvox.NavMathDescription} Updated description.
* @private
*/
cvox.SpeechRuleEngine.prototype.addRelativePersonality_ = function(
nav, personality) {
if (!nav['personality']) {
nav['personality'] = personality;
return nav;
}
var navPersonality = nav['personality'];
for (var p in personality) {
// Although values could exceed boundaries, they will be limited to the
// correct interval via the call to
// cvox.AbstractTts.prototype.mergeProperties in
// cvox.TtsBackground.prototype.speak
if (navPersonality[p] && typeof(navPersonality[p]) == 'number') {
navPersonality[p] = navPersonality[p] + personality[p];
} else {
navPersonality[p] = personality[p];
}
}
return nav;
};
/**
* Resets personalities to default values if necessary.
* @param {cvox.NavDescription|cvox.NavMathDescription} nav Nav Description.
* @private
*/
cvox.SpeechRuleEngine.prototype.resetPersonality_ = function(nav) {
if (this.activeStore_.defaultTtsProps) {
for (var i = 0, prop; prop = this.activeStore_.defaultTtsProps[i]; i++) {
nav.personality[prop] = cvox.ChromeVox.tts.getDefaultProperty(prop);
}
}
};
/**
* Flag for the debug mode of the speech rule engine.
* @type {boolean}
*/
cvox.SpeechRuleEngine.debugMode = false;
/**
* Give debug output.
* @param {...*} output Rest elements of debug output.
*/
cvox.SpeechRuleEngine.outputDebug = function(output) {
if (cvox.SpeechRuleEngine.debugMode) {
var outputList = Array.prototype.slice.call(arguments, 0);
console.log.apply(console,
['Speech Rule Engine Debugger:'].concat(outputList));
}
};
/**
* Prints the list of all current rules in ChromeVox to the console.
* @return {string} A textual representation of all rules in the speech rule
* engine.
*/
cvox.SpeechRuleEngine.prototype.toString = function() {
var allRules = this.activeStore_.findAllRules(function(x) {return true;});
return allRules.map(function(rule) {return rule.toString();}).
join('\n');
};
/**
* Test the precondition of a speech rule in debugging mode.
* @param {cvox.SpeechRule} rule A speech rule.
* @param {!Node} node DOM node to test applicability of the rule.
*/
cvox.SpeechRuleEngine.debugSpeechRule = function(rule, node) {
var store = cvox.SpeechRuleEngine.getInstance().activeStore_;
if (store) {
var prec = rule.precondition;
cvox.SpeechRuleEngine.outputDebug(
prec.query, store.applyQuery(node, prec.query));
prec.constraints.forEach(
function(cstr) {
cvox.SpeechRuleEngine.outputDebug(
cstr, store.applyConstraint(node, cstr));});
}
};
/**
* Test the precondition of a speech rule in debugging mode.
* @param {string} name Rule to debug.
* @param {!Node} node DOM node to test applicability of the rule.
*/
cvox.SpeechRuleEngine.debugNamedSpeechRule = function(name, node) {
var store = cvox.SpeechRuleEngine.getInstance().activeStore_;
var allRules = store.findAllRules(
function(rule) {return rule.name == name;});
for (var i = 0, rule; rule = allRules[i]; i++) {
cvox.SpeechRuleEngine.outputDebug('Rule', name, 'number', i);
cvox.SpeechRuleEngine.debugSpeechRule(rule, node);
}
};