speech_rule.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 An interface definition of a speech rule.
7 *
8 * A speech rule is a data structure along with supporting methods that
9 * stipulates how to transform a tree structure such as XML, a browser DOM, or
10 * HTML into a format (usually strings) suitable for rendering by a
11 * text-to-speech engine.
12 *
13 * Speech rules consists of a variable number of speech rule components. Each
14 * component describes how to construct a single utterance. Text-to-speech
15 * renders the components in order.
16 */
17
18goog.provide('cvox.SpeechRule');
19goog.provide('cvox.SpeechRule.Action');
20goog.provide('cvox.SpeechRule.Component');
21goog.provide('cvox.SpeechRule.DynamicCstr');
22goog.provide('cvox.SpeechRule.Precondition');
23goog.provide('cvox.SpeechRule.Type');
24
25
26/**
27 * Creates a speech rule with precondition, actions and admin information.
28 * @constructor
29 * @param {string} name The name of the rule.
30 * @param {cvox.SpeechRule.DynamicCstr} dynamic Dynamic constraint annotations
31 *     of the rule.
32 * @param {cvox.SpeechRule.Precondition} prec Precondition of the rule.
33 * @param {cvox.SpeechRule.Action} action Action of the speech rule.
34 */
35cvox.SpeechRule = function(name, dynamic, prec, action) {
36  /** @type {string} */
37  this.name = name;
38  /** @type {cvox.SpeechRule.DynamicCstr} */
39  this.dynamicCstr = dynamic;
40  /** @type {cvox.SpeechRule.Precondition} */
41  this.precondition = prec;
42  /** @type {cvox.SpeechRule.Action} */
43  this.action = action;
44};
45
46
47/**
48 *
49 * @override
50 */
51cvox.SpeechRule.prototype.toString = function() {
52  var cstrStrings = [];
53  for (var key in this.dynamicCstr) {
54    cstrStrings.push(this.dynamicCstr[key]);
55  }
56  return this.name + ' | ' + cstrStrings.join('.') + ' | ' +
57    this.precondition.toString() + ' ==> ' +
58    this.action.toString();
59};
60
61
62/**
63 * Mapping for types of speech rule components.
64 * @enum {string}
65 */
66cvox.SpeechRule.Type = {
67  NODE: 'NODE',
68  MULTI: 'MULTI',
69  TEXT: 'TEXT',
70  PERSONALITY: 'PERSONALITY'
71};
72
73
74/**
75 * Maps a string to a valid speech rule type.
76 * @param {string} str Input string.
77 * @return {cvox.SpeechRule.Type}
78 */
79cvox.SpeechRule.Type.fromString = function(str) {
80  switch (str) {
81    case '[n]': return cvox.SpeechRule.Type.NODE;
82    case '[m]': return cvox.SpeechRule.Type.MULTI;
83    case '[t]': return cvox.SpeechRule.Type.TEXT;
84    case '[p]': return cvox.SpeechRule.Type.PERSONALITY;
85    default: throw 'Parse error: ' + str;
86  }
87};
88
89
90/**
91 * Maps a speech rule type to a human-readable string.
92 * @param {cvox.SpeechRule.Type} speechType
93 * @return {string} Output string.
94 */
95cvox.SpeechRule.Type.toString = function(speechType) {
96  switch (speechType) {
97    case cvox.SpeechRule.Type.NODE: return '[n]';
98    case cvox.SpeechRule.Type.MULTI: return '[m]';
99    case cvox.SpeechRule.Type.TEXT: return '[t]';
100    case cvox.SpeechRule.Type.PERSONALITY: return '[p]';
101    default: throw 'Unknown type error: ' + speechType;
102  }
103};
104
105
106/**
107 * Defines a component within a speech rule.
108 * @param {{type: cvox.SpeechRule.Type, content: string}} kwargs The input
109 * component in JSON format.
110 * @constructor
111 */
112cvox.SpeechRule.Component = function(kwargs) {
113  /** @type {cvox.SpeechRule.Type} */
114  this.type = kwargs.type;
115
116  /** @type {string} */
117  this.content = kwargs.content;
118};
119
120
121/**
122 * Parses a valid string representation of a speech component into a Component
123 * object.
124 * @param {string} input The input string.
125 * @return {cvox.SpeechRule.Component} The resulting component.
126 */
127cvox.SpeechRule.Component.fromString = function(input) {
128  // The output JSON.
129  var output = {};
130
131  // Parse the type.
132  output.type = cvox.SpeechRule.Type.fromString(input.substring(0, 3));
133
134  // Prep the rest of the parsing.
135  var rest = input.slice(3).trimLeft();
136  if (!rest) {
137    throw new cvox.SpeechRule.OutputError('Missing content.');
138  }
139
140  switch (output.type) {
141    case cvox.SpeechRule.Type.TEXT:
142      if (rest[0] == '"') {
143        var quotedString = cvox.SpeechRule.splitString_(rest, '\\(')[0].trim();
144        if (quotedString.slice(-1) != '"') {
145          throw new cvox.SpeechRule.OutputError('Invalid string syntax.');
146        }
147        output.content = quotedString;
148        rest = rest.slice(quotedString.length).trim();
149        if (rest.indexOf('(') == -1) {
150          rest = '';
151        }
152        // This break is conditional. If the content is not an explicit string,
153        // it can be treated like node and multi type.
154        break;
155      }
156    case cvox.SpeechRule.Type.NODE:
157    case cvox.SpeechRule.Type.MULTI:
158      var bracket = rest.indexOf(' (');
159      if (bracket == -1) {
160        output.content = rest.trim();
161        rest = '';
162        break;
163      }
164      output.content = rest.substring(0, bracket).trim();
165      rest = rest.slice(bracket).trimLeft();
166    break;
167  }
168  output = new cvox.SpeechRule.Component(output);
169  if (rest) {
170    output.addAttributes(rest);
171  }
172  return output;
173};
174
175
176/**
177 * @override
178 */
179cvox.SpeechRule.Component.prototype.toString = function() {
180  var strs = '';
181  strs += cvox.SpeechRule.Type.toString(this.type);
182  strs += this.content ? ' ' +  this.content : '';
183  var attribs = this.getAttributes();
184  if (attribs.length > 0) {
185    strs += ' (' + attribs.join(', ') + ')';
186  }
187  return strs;
188};
189
190
191/**
192 * Adds a single attribute to the component.
193 * @param {string} attr String representation of an attribute.
194 */
195cvox.SpeechRule.Component.prototype.addAttribute = function(attr) {
196  var colon = attr.indexOf(':');
197  if (colon == -1) {
198    this[attr.trim()] = 'true';
199  } else {
200    this[attr.substring(0, colon).trim()] = attr.slice(colon + 1).trim();
201  }
202};
203
204
205/**
206 * Adds a list of attributes to the component.
207 * @param {string} attrs String representation of attribute list.
208 */
209cvox.SpeechRule.Component.prototype.addAttributes = function(attrs) {
210  if (attrs[0] != '(' || attrs.slice(-1) != ')') {
211    throw new cvox.SpeechRule.OutputError(
212        'Invalid attribute expression: ' + attrs);
213  }
214  var attribs = cvox.SpeechRule.splitString_(attrs.slice(1, -1), ',');
215  for (var i = 0; i < attribs.length; i++) {
216    this.addAttribute(attribs[i]);
217  }
218};
219
220
221/**
222 * Transforms the attributes of an object into a list of strings.
223 * @return {Array.<string>} List of translated attribute:value strings.
224 */
225cvox.SpeechRule.Component.prototype.getAttributes = function() {
226  var attribs = [];
227  for (var key in this) {
228    if (key != 'content' && key != 'type' && typeof(this[key]) != 'function') {
229      attribs.push(key + ':' + this[key]);
230    }
231  }
232  return attribs;
233};
234
235
236/**
237 * A speech rule is a collection of speech components.
238 * @param {Array.<cvox.SpeechRule.Component>} components The input rule.
239 * @constructor
240 */
241cvox.SpeechRule.Action = function(components) {
242  /** @type {Array.<cvox.SpeechRule.Component>} */
243  this.components = components;
244};
245
246
247/**
248 * Parses an input string into a speech rule class object.
249 * @param {string} input The input string.
250 * @return {cvox.SpeechRule.Action} The resulting object.
251 */
252cvox.SpeechRule.Action.fromString = function(input) {
253  var comps = cvox.SpeechRule.splitString_(input, ';')
254      .filter(function(x) {return x.match(/\S/);})
255      .map(function(x) {return x.trim();});
256  var newComps = [];
257  for (var i = 0; i < comps.length; i++) {
258    var comp = cvox.SpeechRule.Component.fromString(comps[i]);
259    if (comp) {
260      newComps.push(comp);
261    }
262  }
263return new cvox.SpeechRule.Action(newComps);
264};
265
266
267/**
268 * @override
269 */
270cvox.SpeechRule.Action.prototype.toString = function() {
271  var comps = this.components.map(function(c) { return c.toString(); });
272  return comps.join('; ');
273};
274
275
276// TODO (sorge) Separatation of xpath expressions and custom functions.
277// Also test validity of xpath expressions.
278/**
279 * Constructs a valid precondition for a speech rule.
280 * @param {string} query A node selector function or xpath expression.
281 * @param {Array.<string>=} opt_constraints A list of constraint functions.
282 * @constructor
283 */
284cvox.SpeechRule.Precondition = function(query, opt_constraints) {
285  /** @type {string} */
286  this.query = query;
287
288  /** @type {!Array.<string>} */
289  this.constraints = opt_constraints || [];
290};
291
292
293/**
294 * @override
295 */
296cvox.SpeechRule.Precondition.prototype.toString = function() {
297  var constrs = this.constraints.join(', ');
298  return this.query + ', ' + constrs;
299};
300
301
302/**
303 * Split a string wrt. a given separator symbol while not splitting inside of a
304 * double quoted string. For example, splitting
305 * '[t] "matrix; 3 by 3"; [n] ./*[1]' with separators ';' would yield
306 * ['[t] "matrix; 3 by 3"', ' [n] ./*[1]'].
307 * @param {string} str String to be split.
308 * @param {string} sep Separator symbol.
309 * @return {Array.<string>} A list of single component strings.
310 * @private
311 */
312cvox.SpeechRule.splitString_ = function(str, sep) {
313  var strList = [];
314  var prefix = '';
315
316  while (str != '') {
317    var sepPos = str.search(sep);
318    if (sepPos == -1) {
319      if ((str.match(/"/g) || []).length % 2 != 0) {
320        throw new cvox.SpeechRule.OutputError(
321            'Invalid string in expression: ' + str);
322      }
323      strList.push(prefix + str);
324      prefix = '';
325      str = '';
326    } else if (
327        (str.substring(0, sepPos).match(/"/g) || []).length % 2 == 0) {
328      strList.push(prefix + str.substring(0, sepPos));
329      prefix = '';
330      str = str.substring(sepPos + 1);
331    } else {
332      var nextQuot = str.substring(sepPos).search('"');
333      if (nextQuot == -1) {
334        throw new cvox.SpeechRule.OutputError(
335            'Invalid string in expression: ' + str);
336      } else {
337        prefix = prefix + str.substring(0, sepPos + nextQuot + 1);
338        str = str.substring(sepPos + nextQuot + 1);
339      }
340    }
341  }
342  if (prefix) {
343    strList.push(prefix);
344  }
345  return strList;
346};
347
348
349/**
350 * Attributes for dynamic constraints.
351 * We define one default attribute as style. Speech rule stores can add other
352 * attributes later.
353 * @enum {string}
354 */
355cvox.SpeechRule.DynamicCstrAttrib =
356{
357  STYLE: 'style'
358};
359
360
361/**
362 * Dynamic constraints are a means to specialize rules that can be changed
363 * dynamically by the user, for example by choosing different styles, etc.
364 * @typedef {!Object.<cvox.SpeechRule.DynamicCstrAttrib, string>}
365 */
366cvox.SpeechRule.DynamicCstr;
367
368
369/**
370 * Error object for signaling parsing errors.
371 * @param {string} msg The error message.
372 * @constructor
373 * @extends {Error}
374 */
375cvox.SpeechRule.OutputError = function(msg) {
376  this.name = 'RuleError';
377  this.message = msg || '';
378};
379goog.inherits(cvox.SpeechRule.OutputError, Error);
380