Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / chrome / browser / resources / chromeos / chromevox / braille / liblouis.js
blob0b8a2ee0006e53a1179493d08567f749e5cd4582
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.
5 /**
6  * @fileoverview JavaScript shim for the liblouis Native Client wrapper.
7  */
9 goog.provide('cvox.LibLouis');
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  */
18 cvox.LibLouis = function(nmfPath, opt_tablesDir) {
19   /**
20    * Path to .nmf file for the module.
21    * @private {string}
22    */
23   this.nmfPath_ = nmfPath;
25   /**
26    * Path to translation tables.
27    * @private {?string}
28    */
29   this.tablesDir_ = goog.isDef(opt_tablesDir) ? opt_tablesDir : null;
31   /**
32    * Native Client <embed> element.
33    * {@code null} when no <embed> is attached to the DOM.
34    * @private {HTMLEmbedElement}
35    */
36   this.embedElement_ = null;
38   /**
39    * Pending RPC callbacks. Maps from message IDs to callbacks.
40    * @private {!Object<function(!Object)>}
41    */
42   this.pendingRpcCallbacks_ = {};
44   /**
45    * Next message ID to be used. Incremented with each sent message.
46    * @private {number}
47    */
48   this.nextMessageId_ = 1;
52 /**
53  * Set to {@code true} to enable debug logging of RPC messages.
54  * @type {boolean}
55  */
56 cvox.LibLouis.DEBUG = false;
59 /**
60  * Attaches the Native Client wrapper to the DOM as a child of the provided
61  * element, assumed to already be in the document.
62  * @param {!Element} elem Desired parent element of the instance.
63  */
64 cvox.LibLouis.prototype.attachToElement = function(elem) {
65   if (this.isAttached()) {
66     throw Error('Instance already attached');
67   }
69   var embed = document.createElement('embed');
70   embed.src = this.nmfPath_;
71   embed.type = 'application/x-nacl';
72   embed.width = 0;
73   embed.height = 0;
74   if (!goog.isNull(this.tablesDir_)) {
75     embed.setAttribute('tablesdir', this.tablesDir_);
76   }
77   embed.addEventListener('load', goog.bind(this.onInstanceLoad_, this),
78       false /* useCapture */);
79   embed.addEventListener('error', goog.bind(this.onInstanceError_, this),
80       false /* useCapture */);
81   embed.addEventListener('message', goog.bind(this.onInstanceMessage_, this),
82       false /* useCapture */);
83   elem.appendChild(embed);
84   this.embedElement_ = /** @type {!HTMLEmbedElement} */ (embed);
88 /**
89  * Detaches the Native Client instance from the DOM.
90  */
91 cvox.LibLouis.prototype.detach = function() {
92   if (!this.isAttached()) {
93     throw Error('cannot detach unattached instance');
94   }
96   this.embedElement_.parentNode.removeChild(this.embedElement_);
97   this.embedElement_ = null;
98   for (var id in this.pendingRpcCallbacks_) {
99     this.pendingRpcCallbacks_[id]({});
100   }
101   this.pendingRpcCallbacks_ = {};
106  * Determines whether the Native Client instance is attached.
107  * @return {boolean} {@code true} if the <embed> element is attached to the DOM.
108  */
109 cvox.LibLouis.prototype.isAttached = function() {
110   return this.embedElement_ !== null;
115  * Returns a translator for the desired table, asynchronously.
116  * This object must be attached to a document when requesting a translator.
117  * @param {string} tableNames Comma separated list of braille table names for
118  *     liblouis.
119  * @param {function(cvox.LibLouis.Translator)} callback
120  *     Callback which will receive the translator, or {@code null} on failure.
121  */
122 cvox.LibLouis.prototype.getTranslator = function(tableNames, callback) {
123   if (!this.isAttached()) {
124     callback(null /* translator */);
125     return;
126   }
127   this.rpc_('CheckTable', { 'table_names': tableNames }, function(reply) {
128     if (reply['success']) {
129       var translator = new cvox.LibLouis.Translator(this, tableNames);
130       callback(translator);
131     } else {
132       callback(null /* translator */);
133     }
134   }.bind(this));
139  * Dispatches a message to the remote end and returns the reply asynchronously.
140  * A message ID will be automatically assigned (as a side-effect).
141  * @param {string} command Command name to be sent.
142  * @param {!Object} message JSONable message to be sent.
143  * @param {function(!Object)} callback Callback to receive the reply.
144  * @private
145  */
146 cvox.LibLouis.prototype.rpc_ =
147     function(command, message, callback) {
148   if (!this.isAttached()) {
149     throw Error('Cannot send RPC: liblouis instance not loaded');
150   }
151   var messageId = '' + this.nextMessageId_++;
152   message['message_id'] = messageId;
153   message['command'] = command;
154   var json = JSON.stringify(message);
155   if (cvox.LibLouis.DEBUG) {
156     window.console.debug('RPC -> ' + json);
157   }
158   this.embedElement_.postMessage(json);
159   this.pendingRpcCallbacks_[messageId] = callback;
164  * Invoked when the Native Client instance successfully loads.
165  * @param {Event} e Event dispatched after loading.
166  * @private
167  */
168 cvox.LibLouis.prototype.onInstanceLoad_ = function(e) {
169   window.console.info('loaded liblouis Native Client instance');
174  * Invoked when the Native Client instance fails to load.
175  * @param {Event} e Event dispatched after loading failure.
176  * @private
177  */
178 cvox.LibLouis.prototype.onInstanceError_ = function(e) {
179   window.console.error('failed to load liblouis Native Client instance');
180   this.detach();
185  * Invoked when the Native Client instance posts a message.
186  * @param {Event} e Event dispatched after the message was posted.
187  * @private
188  */
189 cvox.LibLouis.prototype.onInstanceMessage_ = function(e) {
190   if (cvox.LibLouis.DEBUG) {
191     window.console.debug('RPC <- ' + e.data);
192   }
193   var message = /** @type {!Object} */ (JSON.parse(e.data));
194   var messageId = message['in_reply_to'];
195   if (!goog.isDef(messageId)) {
196     window.console.warn('liblouis Native Client module sent message with no ID',
197         message);
198     return;
199   }
200   if (goog.isDef(message['error'])) {
201     window.console.error('liblouis Native Client error', message['error']);
202   }
203   var callback = this.pendingRpcCallbacks_[messageId];
204   if (goog.isDef(callback)) {
205     delete this.pendingRpcCallbacks_[messageId];
206     callback(message);
207   }
212  * Braille translator which uses a Native Client instance of liblouis.
213  * @constructor
214  * @param {!cvox.LibLouis} instance The instance wrapper.
215  * @param {string} tableNames Comma separated list of Table names to be passed
216  *     to liblouis.
217  */
218 cvox.LibLouis.Translator = function(instance, tableNames) {
219   /**
220    * The instance wrapper.
221    * @private {!cvox.LibLouis}
222    */
223   this.instance_ = instance;
225   /**
226    * The table name.
227    * @private {string}
228    */
229   this.tableNames_ = tableNames;
234  * Translates text into braille cells.
235  * @param {string} text Text to be translated.
236  * @param {function(ArrayBuffer, Array<number>, Array<number>)} callback
237  *     Callback for result.  Takes 3 parameters: the resulting cells,
238  *     mapping from text to braille positions and mapping from braille to
239  *     text positions.  If translation fails for any reason, all parameters are
240  *     {@code null}.
241  */
242 cvox.LibLouis.Translator.prototype.translate = function(text, callback) {
243   if (!this.instance_.isAttached()) {
244     callback(null /*cells*/, null /*textToBraille*/, null /*brailleToText*/);
245     return;
246   }
247   var message = { 'table_names': this.tableNames_, 'text': text };
248   this.instance_.rpc_('Translate', message, function(reply) {
249     var cells = null;
250     var textToBraille = null;
251     var brailleToText = null;
252     if (reply['success'] && goog.isString(reply['cells'])) {
253       cells = cvox.LibLouis.Translator.decodeHexString_(reply['cells']);
254       if (goog.isDef(reply['text_to_braille'])) {
255         textToBraille = reply['text_to_braille'];
256       }
257       if (goog.isDef(reply['braille_to_text'])) {
258         brailleToText = reply['braille_to_text'];
259       }
260     } else if (text.length > 0) {
261       // TODO(plundblad): The nacl wrapper currently returns an error
262       // when translating an empty string.  Address that and always log here.
263       console.error('Braille translation error for ' + JSON.stringify(message));
264     }
265     callback(cells, textToBraille, brailleToText);
266   });
271  * Translates braille cells into text.
272  * @param {!ArrayBuffer} cells Cells to be translated.
273  * @param {function(?string)} callback Callback for result.
274  */
275 cvox.LibLouis.Translator.prototype.backTranslate =
276     function(cells, callback) {
277   if (!this.instance_.isAttached()) {
278     callback(null /*text*/);
279     return;
280   }
281   if (cells.byteLength == 0) {
282     // liblouis doesn't handle empty input, so handle that trivially
283     // here.
284     callback('');
285     return;
286   }
287   var message = {
288     'table_names': this.tableNames_,
289     'cells': cvox.LibLouis.Translator.encodeHexString_(cells)
290   };
291   this.instance_.rpc_('BackTranslate', message, function(reply) {
292     if (reply['success'] && goog.isString(reply['text'])) {
293       callback(reply['text']);
294     } else {
295       callback(null /* text */);
296     }
297   });
302  * Decodes a hexadecimal string to an {@code ArrayBuffer}.
303  * @param {string} hex Hexadecimal string.
304  * @return {!ArrayBuffer} Decoded binary data.
305  * @private
306  */
307 cvox.LibLouis.Translator.decodeHexString_ = function(hex) {
308   if (!/^([0-9a-f]{2})*$/i.test(hex)) {
309     throw Error('invalid hexadecimal string');
310   }
311   var array = new Uint8Array(hex.length / 2);
312   var idx = 0;
313   for (var i = 0; i < hex.length; i += 2) {
314     array[idx++] = parseInt(hex.substring(i, i + 2), 16);
315   }
316   return array.buffer;
321  * Encodes an {@code ArrayBuffer} in hexadecimal.
322  * @param {!ArrayBuffer} arrayBuffer Binary data.
323  * @return {string} Hexadecimal string.
324  * @private
325  */
326 cvox.LibLouis.Translator.encodeHexString_ = function(arrayBuffer) {
327   var array = new Uint8Array(arrayBuffer);
328   var hex = '';
329   for (var i = 0; i < array.length; i++) {
330     var b = array[i];
331     hex += (b < 0x10 ? '0' : '') + b.toString(16);
332   }
333   return hex;