// Copyright 2014 The Chromium Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /** * @fileoverview An interface definition of a speech rule. * * A speech rule is a data structure along with supporting methods that * stipulates how to transform a tree structure such as XML, a browser DOM, or * HTML into a format (usually strings) suitable for rendering by a * text-to-speech engine. * * Speech rules consists of a variable number of speech rule components. Each * component describes how to construct a single utterance. Text-to-speech * renders the components in order. */ goog.provide('cvox.SpeechRule'); goog.provide('cvox.SpeechRule.Action'); goog.provide('cvox.SpeechRule.Component'); goog.provide('cvox.SpeechRule.DynamicCstr'); goog.provide('cvox.SpeechRule.Precondition'); goog.provide('cvox.SpeechRule.Type'); /** * Creates a speech rule with precondition, actions and admin information. * @constructor * @param {string} name The name of the rule. * @param {cvox.SpeechRule.DynamicCstr} dynamic Dynamic constraint annotations * of the rule. * @param {cvox.SpeechRule.Precondition} prec Precondition of the rule. * @param {cvox.SpeechRule.Action} action Action of the speech rule. */ cvox.SpeechRule = function(name, dynamic, prec, action) { /** @type {string} */ this.name = name; /** @type {cvox.SpeechRule.DynamicCstr} */ this.dynamicCstr = dynamic; /** @type {cvox.SpeechRule.Precondition} */ this.precondition = prec; /** @type {cvox.SpeechRule.Action} */ this.action = action; }; /** * * @override */ cvox.SpeechRule.prototype.toString = function() { var cstrStrings = []; for (var key in this.dynamicCstr) { cstrStrings.push(this.dynamicCstr[key]); } return this.name + ' | ' + cstrStrings.join('.') + ' | ' + this.precondition.toString() + ' ==> ' + this.action.toString(); }; /** * Mapping for types of speech rule components. * @enum {string} */ cvox.SpeechRule.Type = { NODE: 'NODE', MULTI: 'MULTI', TEXT: 'TEXT', PERSONALITY: 'PERSONALITY' }; /** * Maps a string to a valid speech rule type. * @param {string} str Input string. * @return {cvox.SpeechRule.Type} */ cvox.SpeechRule.Type.fromString = function(str) { switch (str) { case '[n]': return cvox.SpeechRule.Type.NODE; case '[m]': return cvox.SpeechRule.Type.MULTI; case '[t]': return cvox.SpeechRule.Type.TEXT; case '[p]': return cvox.SpeechRule.Type.PERSONALITY; default: throw 'Parse error: ' + str; } }; /** * Maps a speech rule type to a human-readable string. * @param {cvox.SpeechRule.Type} speechType * @return {string} Output string. */ cvox.SpeechRule.Type.toString = function(speechType) { switch (speechType) { case cvox.SpeechRule.Type.NODE: return '[n]'; case cvox.SpeechRule.Type.MULTI: return '[m]'; case cvox.SpeechRule.Type.TEXT: return '[t]'; case cvox.SpeechRule.Type.PERSONALITY: return '[p]'; default: throw 'Unknown type error: ' + speechType; } }; /** * Defines a component within a speech rule. * @param {{type: cvox.SpeechRule.Type, content: string}} kwargs The input * component in JSON format. * @constructor */ cvox.SpeechRule.Component = function(kwargs) { /** @type {cvox.SpeechRule.Type} */ this.type = kwargs.type; /** @type {string} */ this.content = kwargs.content; }; /** * Parses a valid string representation of a speech component into a Component * object. * @param {string} input The input string. * @return {cvox.SpeechRule.Component} The resulting component. */ cvox.SpeechRule.Component.fromString = function(input) { // The output JSON. var output = {}; // Parse the type. output.type = cvox.SpeechRule.Type.fromString(input.substring(0, 3)); // Prep the rest of the parsing. var rest = input.slice(3).trimLeft(); if (!rest) { throw new cvox.SpeechRule.OutputError('Missing content.'); } switch (output.type) { case cvox.SpeechRule.Type.TEXT: if (rest[0] == '"') { var quotedString = cvox.SpeechRule.splitString_(rest, '\\(')[0].trim(); if (quotedString.slice(-1) != '"') { throw new cvox.SpeechRule.OutputError('Invalid string syntax.'); } output.content = quotedString; rest = rest.slice(quotedString.length).trim(); if (rest.indexOf('(') == -1) { rest = ''; } // This break is conditional. If the content is not an explicit string, // it can be treated like node and multi type. break; } case cvox.SpeechRule.Type.NODE: case cvox.SpeechRule.Type.MULTI: var bracket = rest.indexOf(' ('); if (bracket == -1) { output.content = rest.trim(); rest = ''; break; } output.content = rest.substring(0, bracket).trim(); rest = rest.slice(bracket).trimLeft(); break; } output = new cvox.SpeechRule.Component(output); if (rest) { output.addAttributes(rest); } return output; }; /** * @override */ cvox.SpeechRule.Component.prototype.toString = function() { var strs = ''; strs += cvox.SpeechRule.Type.toString(this.type); strs += this.content ? ' ' + this.content : ''; var attribs = this.getAttributes(); if (attribs.length > 0) { strs += ' (' + attribs.join(', ') + ')'; } return strs; }; /** * Adds a single attribute to the component. * @param {string} attr String representation of an attribute. */ cvox.SpeechRule.Component.prototype.addAttribute = function(attr) { var colon = attr.indexOf(':'); if (colon == -1) { this[attr.trim()] = 'true'; } else { this[attr.substring(0, colon).trim()] = attr.slice(colon + 1).trim(); } }; /** * Adds a list of attributes to the component. * @param {string} attrs String representation of attribute list. */ cvox.SpeechRule.Component.prototype.addAttributes = function(attrs) { if (attrs[0] != '(' || attrs.slice(-1) != ')') { throw new cvox.SpeechRule.OutputError( 'Invalid attribute expression: ' + attrs); } var attribs = cvox.SpeechRule.splitString_(attrs.slice(1, -1), ','); for (var i = 0; i < attribs.length; i++) { this.addAttribute(attribs[i]); } }; /** * Transforms the attributes of an object into a list of strings. * @return {Array} List of translated attribute:value strings. */ cvox.SpeechRule.Component.prototype.getAttributes = function() { var attribs = []; for (var key in this) { if (key != 'content' && key != 'type' && typeof(this[key]) != 'function') { attribs.push(key + ':' + this[key]); } } return attribs; }; /** * A speech rule is a collection of speech components. * @param {Array} components The input rule. * @constructor */ cvox.SpeechRule.Action = function(components) { /** @type {Array} */ this.components = components; }; /** * Parses an input string into a speech rule class object. * @param {string} input The input string. * @return {cvox.SpeechRule.Action} The resulting object. */ cvox.SpeechRule.Action.fromString = function(input) { var comps = cvox.SpeechRule.splitString_(input, ';') .filter(function(x) {return x.match(/\S/);}) .map(function(x) {return x.trim();}); var newComps = []; for (var i = 0; i < comps.length; i++) { var comp = cvox.SpeechRule.Component.fromString(comps[i]); if (comp) { newComps.push(comp); } } return new cvox.SpeechRule.Action(newComps); }; /** * @override */ cvox.SpeechRule.Action.prototype.toString = function() { var comps = this.components.map(function(c) { return c.toString(); }); return comps.join('; '); }; // TODO (sorge) Separatation of xpath expressions and custom functions. // Also test validity of xpath expressions. /** * Constructs a valid precondition for a speech rule. * @param {string} query A node selector function or xpath expression. * @param {Array=} opt_constraints A list of constraint functions. * @constructor */ cvox.SpeechRule.Precondition = function(query, opt_constraints) { /** @type {string} */ this.query = query; /** @type {!Array} */ this.constraints = opt_constraints || []; }; /** * @override */ cvox.SpeechRule.Precondition.prototype.toString = function() { var constrs = this.constraints.join(', '); return this.query + ', ' + constrs; }; /** * Split a string wrt. a given separator symbol while not splitting inside of a * double quoted string. For example, splitting * '[t] "matrix; 3 by 3"; [n] ./*[1]' with separators ';' would yield * ['[t] "matrix; 3 by 3"', ' [n] ./*[1]']. * @param {string} str String to be split. * @param {string} sep Separator symbol. * @return {Array} A list of single component strings. * @private */ cvox.SpeechRule.splitString_ = function(str, sep) { var strList = []; var prefix = ''; while (str != '') { var sepPos = str.search(sep); if (sepPos == -1) { if ((str.match(/"/g) || []).length % 2 != 0) { throw new cvox.SpeechRule.OutputError( 'Invalid string in expression: ' + str); } strList.push(prefix + str); prefix = ''; str = ''; } else if ( (str.substring(0, sepPos).match(/"/g) || []).length % 2 == 0) { strList.push(prefix + str.substring(0, sepPos)); prefix = ''; str = str.substring(sepPos + 1); } else { var nextQuot = str.substring(sepPos).search('"'); if (nextQuot == -1) { throw new cvox.SpeechRule.OutputError( 'Invalid string in expression: ' + str); } else { prefix = prefix + str.substring(0, sepPos + nextQuot + 1); str = str.substring(sepPos + nextQuot + 1); } } } if (prefix) { strList.push(prefix); } return strList; }; /** * Attributes for dynamic constraints. * We define one default attribute as style. Speech rule stores can add other * attributes later. * @enum {string} */ cvox.SpeechRule.DynamicCstrAttrib = { STYLE: 'style' }; /** * Dynamic constraints are a means to specialize rules that can be changed * dynamically by the user, for example by choosing different styles, etc. * @typedef {!Object} */ cvox.SpeechRule.DynamicCstr; /** * Error object for signaling parsing errors. * @param {string} msg The error message. * @constructor * @extends {Error} */ cvox.SpeechRule.OutputError = function(msg) { this.name = 'RuleError'; this.message = msg || ''; }; goog.inherits(cvox.SpeechRule.OutputError, Error);