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 9 goog.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 */ 18 cvox.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 */ 70 cvox.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 */ 83 cvox.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 */ 112 cvox.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 */ 128 cvox.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 */ 140 cvox.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 */ 175 cvox.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 */ 198 cvox.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 */ 213 cvox.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 */ 228 cvox.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 */ 257 cvox.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 */ 281 cvox.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 */ 310 cvox.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 */ 338 cvox.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 */ 357 cvox.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