liblouis.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 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 var message = { 311 'table_name': this.tableName_, 312 'cells': cvox.LibLouis.Translator.encodeHexString_(cells) 313 }; 314 this.instance_.rpc_('BackTranslate', message, function(reply) { 315 if (reply['success'] && goog.isString(reply['text'])) { 316 callback(reply['text']); 317 } else { 318 callback(null /* text */); 319 } 320 }); 321}; 322 323 324/** 325 * Decodes a hexadecimal string to an {@code ArrayBuffer}. 326 * @param {string} hex Hexadecimal string. 327 * @return {!ArrayBuffer} Decoded binary data. 328 * @private 329 */ 330cvox.LibLouis.Translator.decodeHexString_ = function(hex) { 331 if (!/^([0-9a-f]{2})*$/i.test(hex)) { 332 throw Error('invalid hexadecimal string'); 333 } 334 var array = new Uint8Array(hex.length / 2); 335 var idx = 0; 336 for (var i = 0; i < hex.length; i += 2) { 337 array[idx++] = parseInt(hex.substring(i, i + 2), 16); 338 } 339 return array.buffer; 340}; 341 342 343/** 344 * Encodes an {@code ArrayBuffer} in hexadecimal. 345 * @param {!ArrayBuffer} arrayBuffer Binary data. 346 * @return {string} Hexadecimal string. 347 * @private 348 */ 349cvox.LibLouis.Translator.encodeHexString_ = function(arrayBuffer) { 350 var array = new Uint8Array(arrayBuffer); 351 var hex = ''; 352 for (var i = 0; i < array.length; i++) { 353 var b = array[i]; 354 hex += (b < 0x10 ? '0' : '') + b.toString(16); 355 } 356 return hex; 357}; 358