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 JavaScript shim for the liblouis Native Client wrapper.
7 */
8
9goog.provide('cvox.LibLouis');
10
11
12/**
13 * Encapsulates a liblouis Native Client instance in the page.
14 * @constructor
15 * @param {string} nmfPath Path to .nmf file for the module.
16 * @param {string=} opt_tablesDir Path to tables directory.
17 */
18cvox.LibLouis = function(nmfPath, opt_tablesDir) {
19  /**
20   * Path to .nmf file for the module.
21   * @private {string}
22   */
23  this.nmfPath_ = nmfPath;
24
25  /**
26   * Path to translation tables.
27   * @private {?string}
28   */
29  this.tablesDir_ = goog.isDef(opt_tablesDir) ? opt_tablesDir : null;
30
31  /**
32   * Native Client <embed> element.
33   * {@code null} when no <embed> is attached to the DOM.
34   * @private {NaClEmbedElement}
35   */
36  this.embedElement_ = null;
37
38  /**
39   * The state of the instance.
40   * @private {cvox.LibLouis.InstanceState}
41   */
42  this.instanceState_ =
43      cvox.LibLouis.InstanceState.NOT_LOADED;
44
45  /**
46   * Pending requests to construct translators.
47   * @private {!Array.<{tableName: string,
48   *   callback: function(cvox.LibLouis.Translator)}>}
49   */
50  this.pendingTranslators_ = [];
51
52  /**
53   * Pending RPC callbacks. Maps from message IDs to callbacks.
54   * @private {!Object.<string, function(!Object)>}
55   */
56  this.pendingRpcCallbacks_ = {};
57
58  /**
59   * Next message ID to be used. Incremented with each sent message.
60   * @private {number}
61   */
62  this.nextMessageId_ = 1;
63};
64
65
66/**
67 * Describes the loading state of the instance.
68 * @enum {number}
69 */
70cvox.LibLouis.InstanceState = {
71  NOT_LOADED: 0,
72  LOADING: 1,
73  LOADED: 2,
74  ERROR: -1
75};
76
77
78/**
79 * Attaches the Native Client wrapper to the DOM as a child of the provided
80 * element, assumed to already be in the document.
81 * @param {!Element} elem Desired parent element of the instance.
82 */
83cvox.LibLouis.prototype.attachToElement = function(elem) {
84  if (this.isAttached()) {
85    throw Error('instance already attached');
86  }
87
88  var embed = document.createElement('embed');
89  embed.src = this.nmfPath_;
90  embed.type = 'application/x-nacl';
91  embed.width = 0;
92  embed.height = 0;
93  if (!goog.isNull(this.tablesDir_)) {
94    embed.setAttribute('tablesdir', this.tablesDir_);
95  }
96  embed.addEventListener('load', goog.bind(this.onInstanceLoad_, this),
97      false /* useCapture */);
98  embed.addEventListener('error', goog.bind(this.onInstanceError_, this),
99      false /* useCapture */);
100  embed.addEventListener('message', goog.bind(this.onInstanceMessage_, this),
101      false /* useCapture */);
102  elem.appendChild(embed);
103
104  this.embedElement_ = /** @type {!NaClEmbedElement} */ (embed);
105  this.instanceState_ = cvox.LibLouis.InstanceState.LOADING;
106};
107
108
109/**
110 * Detaches the Native Client instance from the DOM.
111 */
112cvox.LibLouis.prototype.detach = function() {
113  if (!this.isAttached()) {
114    throw Error('cannot detach unattached instance');
115  }
116
117  this.embedElement_.parentNode.removeChild(this.embedElement_);
118  this.embedElement_ = null;
119  this.instanceState_ =
120      cvox.LibLouis.InstanceState.NOT_LOADED;
121};
122
123
124/**
125 * Determines whether the Native Client instance is attached.
126 * @return {boolean} {@code true} if the <embed> element is attached to the DOM.
127 */
128cvox.LibLouis.prototype.isAttached = function() {
129  return this.embedElement_ !== null;
130};
131
132
133/**
134 * Returns a translator for the desired table, asynchronously.
135 * @param {string} tableNames Comma separated list of braille table names for
136 *     liblouis.
137 * @param {function(cvox.LibLouis.Translator)} callback
138 *     Callback which will receive the translator, or {@code null} on failure.
139 */
140cvox.LibLouis.prototype.getTranslator =
141    function(tableNames, callback) {
142  switch (this.instanceState_) {
143    case cvox.LibLouis.InstanceState.NOT_LOADED:
144    case cvox.LibLouis.InstanceState.LOADING:
145      this.pendingTranslators_.push(
146          { tableNames: tableNames, callback: callback });
147      return;
148    case cvox.LibLouis.InstanceState.ERROR:
149      callback(null /* translator */);
150      return;
151    case cvox.LibLouis.InstanceState.LOADED:
152      this.rpc_('CheckTable', { 'table_names': tableNames },
153          goog.bind(function(reply) {
154        if (reply['success']) {
155          var translator = new cvox.LibLouis.Translator(
156              this, tableNames);
157          callback(translator);
158        } else {
159          callback(null /* translator */);
160        }
161      }, this));
162      return;
163  }
164};
165
166
167/**
168 * Dispatches a message to the remote end and returns the reply asynchronously.
169 * A message ID will be automatically assigned (as a side-effect).
170 * @param {string} command Command name to be sent.
171 * @param {!Object} message JSONable message to be sent.
172 * @param {function(!Object)} callback Callback to receive the reply.
173 * @private
174 */
175cvox.LibLouis.prototype.rpc_ =
176    function(command, message, callback) {
177  if (this.instanceState_ !==
178      cvox.LibLouis.InstanceState.LOADED) {
179    throw Error('cannot send RPC: liblouis instance not loaded');
180  }
181  var messageId = '' + this.nextMessageId_++;
182  message['message_id'] = messageId;
183  message['command'] = command;
184  var json = JSON.stringify(message);
185  if (goog.DEBUG) {
186    window.console.debug('RPC -> ' + json);
187  }
188  this.embedElement_.postMessage(json);
189  this.pendingRpcCallbacks_[messageId] = callback;
190};
191
192
193/**
194 * Invoked when the Native Client instance successfully loads.
195 * @param {Event} e Event dispatched after loading.
196 * @private
197 */
198cvox.LibLouis.prototype.onInstanceLoad_ = function(e) {
199  window.console.info('loaded liblouis Native Client instance');
200  this.instanceState_ = cvox.LibLouis.InstanceState.LOADED;
201  this.pendingTranslators_.forEach(goog.bind(function(record) {
202    this.getTranslator(record.tableNames, record.callback);
203  }, this));
204  this.pendingTranslators_.length = 0;
205};
206
207
208/**
209 * Invoked when the Native Client instance fails to load.
210 * @param {Event} e Event dispatched after loading failure.
211 * @private
212 */
213cvox.LibLouis.prototype.onInstanceError_ = function(e) {
214  window.console.error('failed to load liblouis Native Client instance');
215  this.instanceState_ = cvox.LibLouis.InstanceState.ERROR;
216  this.pendingTranslators_.forEach(goog.bind(function(record) {
217    this.getTranslator(record.tableNames, record.callback);
218  }, this));
219  this.pendingTranslators_.length = 0;
220};
221
222
223/**
224 * Invoked when the Native Client instance posts a message.
225 * @param {Event} e Event dispatched after the message was posted.
226 * @private
227 */
228cvox.LibLouis.prototype.onInstanceMessage_ = function(e) {
229  if (goog.DEBUG) {
230    window.console.debug('RPC <- ' + e.data);
231  }
232  var message = /** @type {!Object} */ (JSON.parse(e.data));
233  var messageId = message['in_reply_to'];
234  if (!goog.isDef(messageId)) {
235    window.console.warn('liblouis Native Client module sent message with no ID',
236        message);
237    return;
238  }
239  if (goog.isDef(message['error'])) {
240    window.console.error('liblouis Native Client error', message['error']);
241  }
242  var callback = this.pendingRpcCallbacks_[messageId];
243  if (goog.isDef(callback)) {
244    delete this.pendingRpcCallbacks_[messageId];
245    callback(message);
246  }
247};
248
249
250/**
251 * Braille translator which uses a Native Client instance of liblouis.
252 * @constructor
253 * @param {!cvox.LibLouis} instance The instance wrapper.
254 * @param {string} tableNames Comma separated list of Table names to be passed
255 *     to liblouis.
256 */
257cvox.LibLouis.Translator = function(instance, tableNames) {
258  /**
259   * The instance wrapper.
260   * @private {!cvox.LibLouis}
261   */
262  this.instance_ = instance;
263
264  /**
265   * The table name.
266   * @private {string}
267   */
268  this.tableNames_ = tableNames;
269};
270
271
272/**
273 * Translates text into braille cells.
274 * @param {string} text Text to be translated.
275 * @param {function(ArrayBuffer, Array.<number>, Array.<number>)} callback
276 *     Callback for result.  Takes 3 parameters: the resulting cells,
277 *     mapping from text to braille positions and mapping from braille to
278 *     text positions.  If translation fails for any reason, all parameters are
279 *     {@code null}.
280 */
281cvox.LibLouis.Translator.prototype.translate = function(text, callback) {
282  var message = { 'table_names': this.tableNames_, 'text': text };
283  this.instance_.rpc_('Translate', message, function(reply) {
284    var cells = null;
285    var textToBraille = null;
286    var brailleToText = null;
287    if (reply['success'] && goog.isString(reply['cells'])) {
288      cells = cvox.LibLouis.Translator.decodeHexString_(reply['cells']);
289      if (goog.isDef(reply['text_to_braille'])) {
290        textToBraille = reply['text_to_braille'];
291      }
292      if (goog.isDef(reply['braille_to_text'])) {
293        brailleToText = reply['braille_to_text'];
294      }
295    } else if (text.length > 0) {
296      // TODO(plundblad): The nacl wrapper currently returns an error
297      // when translating an empty string.  Address that and always log here.
298      console.error('Braille translation error for ' + JSON.stringify(message));
299    }
300    callback(cells, textToBraille, brailleToText);
301  });
302};
303
304
305/**
306 * Translates braille cells into text.
307 * @param {!ArrayBuffer} cells Cells to be translated.
308 * @param {function(?string)} callback Callback for result.
309 */
310cvox.LibLouis.Translator.prototype.backTranslate =
311    function(cells, callback) {
312  if (cells.byteLength == 0) {
313    // liblouis doesn't handle empty input, so handle that trivially
314    // here.
315    callback('');
316    return;
317  }
318  var message = {
319    'table_names': this.tableNames_,
320    'cells': cvox.LibLouis.Translator.encodeHexString_(cells)
321  };
322  this.instance_.rpc_('BackTranslate', message, function(reply) {
323    if (reply['success'] && goog.isString(reply['text'])) {
324      callback(reply['text']);
325    } else {
326      callback(null /* text */);
327    }
328  });
329};
330
331
332/**
333 * Decodes a hexadecimal string to an {@code ArrayBuffer}.
334 * @param {string} hex Hexadecimal string.
335 * @return {!ArrayBuffer} Decoded binary data.
336 * @private
337 */
338cvox.LibLouis.Translator.decodeHexString_ = function(hex) {
339  if (!/^([0-9a-f]{2})*$/i.test(hex)) {
340    throw Error('invalid hexadecimal string');
341  }
342  var array = new Uint8Array(hex.length / 2);
343  var idx = 0;
344  for (var i = 0; i < hex.length; i += 2) {
345    array[idx++] = parseInt(hex.substring(i, i + 2), 16);
346  }
347  return array.buffer;
348};
349
350
351/**
352 * Encodes an {@code ArrayBuffer} in hexadecimal.
353 * @param {!ArrayBuffer} arrayBuffer Binary data.
354 * @return {string} Hexadecimal string.
355 * @private
356 */
357cvox.LibLouis.Translator.encodeHexString_ = function(arrayBuffer) {
358  var array = new Uint8Array(arrayBuffer);
359  var hex = '';
360  for (var i = 0; i < array.length; i++) {
361    var b = array[i];
362    hex += (b < 0x10 ? '0' : '') + b.toString(16);
363  }
364  return hex;
365};
366