liblouis.js revision 116680a4aac90f2aa7413d9095a592090648e557
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} tableName Braille table name for liblouis.
136 * @param {function(cvox.LibLouis.Translator)} callback
137 *     Callback which will receive the translator, or {@code null} on failure.
138 */
139cvox.LibLouis.prototype.getTranslator =
140    function(tableName, callback) {
141  switch (this.instanceState_) {
142    case cvox.LibLouis.InstanceState.NOT_LOADED:
143    case cvox.LibLouis.InstanceState.LOADING:
144      this.pendingTranslators_.push(
145          { tableName: tableName, callback: callback });
146      return;
147    case cvox.LibLouis.InstanceState.ERROR:
148      callback(null /* translator */);
149      return;
150    case cvox.LibLouis.InstanceState.LOADED:
151      this.rpc_('CheckTable', { 'table_name': tableName },
152          goog.bind(function(reply) {
153        if (reply['success']) {
154          var translator = new cvox.LibLouis.Translator(
155              this, tableName);
156          callback(translator);
157        } else {
158          callback(null /* translator */);
159        }
160      }, this));
161      return;
162  }
163};
164
165
166/**
167 * Dispatches a message to the remote end and returns the reply asynchronously.
168 * A message ID will be automatically assigned (as a side-effect).
169 * @param {string} command Command name to be sent.
170 * @param {!Object} message JSONable message to be sent.
171 * @param {function(!Object)} callback Callback to receive the reply.
172 * @private
173 */
174cvox.LibLouis.prototype.rpc_ =
175    function(command, message, callback) {
176  if (this.instanceState_ !==
177      cvox.LibLouis.InstanceState.LOADED) {
178    throw Error('cannot send RPC: liblouis instance not loaded');
179  }
180  var messageId = '' + this.nextMessageId_++;
181  message['message_id'] = messageId;
182  message['command'] = command;
183  var json = JSON.stringify(message);
184  if (goog.DEBUG) {
185    window.console.debug('RPC -> ' + json);
186  }
187  this.embedElement_.postMessage(json);
188  this.pendingRpcCallbacks_[messageId] = callback;
189};
190
191
192/**
193 * Invoked when the Native Client instance successfully loads.
194 * @param {Event} e Event dispatched after loading.
195 * @private
196 */
197cvox.LibLouis.prototype.onInstanceLoad_ = function(e) {
198  window.console.info('loaded liblouis Native Client instance');
199  this.instanceState_ = cvox.LibLouis.InstanceState.LOADED;
200  this.pendingTranslators_.forEach(goog.bind(function(record) {
201    this.getTranslator(record.tableName, record.callback);
202  }, this));
203  this.pendingTranslators_.length = 0;
204};
205
206
207/**
208 * Invoked when the Native Client instance fails to load.
209 * @param {Event} e Event dispatched after loading failure.
210 * @private
211 */
212cvox.LibLouis.prototype.onInstanceError_ = function(e) {
213  window.console.error('failed to load liblouis Native Client instance');
214  this.instanceState_ = cvox.LibLouis.InstanceState.ERROR;
215  this.pendingTranslators_.forEach(goog.bind(function(record) {
216    this.getTranslator(record.tableName, record.callback);
217  }, this));
218  this.pendingTranslators_.length = 0;
219};
220
221
222/**
223 * Invoked when the Native Client instance posts a message.
224 * @param {Event} e Event dispatched after the message was posted.
225 * @private
226 */
227cvox.LibLouis.prototype.onInstanceMessage_ = function(e) {
228  if (goog.DEBUG) {
229    window.console.debug('RPC <- ' + e.data);
230  }
231  var message = /** @type {!Object} */ (JSON.parse(e.data));
232  var messageId = message['in_reply_to'];
233  if (!goog.isDef(messageId)) {
234    window.console.warn('liblouis Native Client module sent message with no ID',
235        message);
236    return;
237  }
238  if (goog.isDef(message['error'])) {
239    window.console.error('liblouis Native Client error', message['error']);
240  }
241  var callback = this.pendingRpcCallbacks_[messageId];
242  if (goog.isDef(callback)) {
243    delete this.pendingRpcCallbacks_[messageId];
244    callback(message);
245  }
246};
247
248
249/**
250 * Braille translator which uses a Native Client instance of liblouis.
251 * @constructor
252 * @param {!cvox.LibLouis} instance The instance wrapper.
253 * @param {string} tableName The table name to be passed to liblouis.
254 */
255cvox.LibLouis.Translator = function(instance, tableName) {
256  /**
257   * The instance wrapper.
258   * @private {!cvox.LibLouis}
259   */
260  this.instance_ = instance;
261
262  /**
263   * The table name.
264   * @private {string}
265   */
266  this.tableName_ = tableName;
267};
268
269
270/**
271 * Translates text into braille cells.
272 * @param {string} text Text to be translated.
273 * @param {function(ArrayBuffer, Array.<number>, Array.<number>)} callback
274 *     Callback for result.  Takes 3 parameters: the resulting cells,
275 *     mapping from text to braille positions and mapping from braille to
276 *     text positions.  If translation fails for any reason, all parameters are
277 *     {@code null}.
278 */
279cvox.LibLouis.Translator.prototype.translate = function(text, callback) {
280  var message = { 'table_name': this.tableName_, 'text': text };
281  this.instance_.rpc_('Translate', message, function(reply) {
282    var cells = null;
283    var textToBraille = null;
284    var brailleToText = null;
285    if (reply['success'] && goog.isString(reply['cells'])) {
286      cells = cvox.LibLouis.Translator.decodeHexString_(reply['cells']);
287      if (goog.isDef(reply['text_to_braille'])) {
288        textToBraille = reply['text_to_braille'];
289      }
290      if (goog.isDef(reply['braille_to_text'])) {
291        brailleToText = reply['braille_to_text'];
292      }
293    } else if (text.length > 0) {
294      // TODO(plundblad): The nacl wrapper currently returns an error
295      // when translating an empty string.  Address that and always log here.
296      console.error('Braille translation error for ' + JSON.stringify(message));
297    }
298    callback(cells, textToBraille, brailleToText);
299  });
300};
301
302
303/**
304 * Translates braille cells into text.
305 * @param {!ArrayBuffer} cells Cells to be translated.
306 * @param {function(?string)} callback Callback for result.
307 */
308cvox.LibLouis.Translator.prototype.backTranslate =
309    function(cells, callback) {
310  if (cells.byteLength == 0) {
311    // liblouis doesn't handle empty input, so handle that trivially
312    // here.
313    callback('');
314    return;
315  }
316  var message = {
317    'table_name': this.tableName_,
318    'cells': cvox.LibLouis.Translator.encodeHexString_(cells)
319  };
320  this.instance_.rpc_('BackTranslate', message, function(reply) {
321    if (reply['success'] && goog.isString(reply['text'])) {
322      callback(reply['text']);
323    } else {
324      callback(null /* text */);
325    }
326  });
327};
328
329
330/**
331 * Decodes a hexadecimal string to an {@code ArrayBuffer}.
332 * @param {string} hex Hexadecimal string.
333 * @return {!ArrayBuffer} Decoded binary data.
334 * @private
335 */
336cvox.LibLouis.Translator.decodeHexString_ = function(hex) {
337  if (!/^([0-9a-f]{2})*$/i.test(hex)) {
338    throw Error('invalid hexadecimal string');
339  }
340  var array = new Uint8Array(hex.length / 2);
341  var idx = 0;
342  for (var i = 0; i < hex.length; i += 2) {
343    array[idx++] = parseInt(hex.substring(i, i + 2), 16);
344  }
345  return array.buffer;
346};
347
348
349/**
350 * Encodes an {@code ArrayBuffer} in hexadecimal.
351 * @param {!ArrayBuffer} arrayBuffer Binary data.
352 * @return {string} Hexadecimal string.
353 * @private
354 */
355cvox.LibLouis.Translator.encodeHexString_ = function(arrayBuffer) {
356  var array = new Uint8Array(arrayBuffer);
357  var hex = '';
358  for (var i = 0; i < array.length; i++) {
359    var b = array[i];
360    hex += (b < 0x10 ? '0' : '') + b.toString(16);
361  }
362  return hex;
363};
364