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.
6 * @fileoverview JavaScript shim for the liblouis Native Client wrapper.
9 goog.provide('cvox.LibLouis');
13 * Encapsulates a liblouis Native Client instance in the page.
15 * @param {string} nmfPath Path to .nmf file for the module.
16 * @param {string=} opt_tablesDir Path to tables directory.
18 cvox.LibLouis = function(nmfPath, opt_tablesDir) {
20 * Path to .nmf file for the module.
23 this.nmfPath_ = nmfPath;
26 * Path to translation tables.
29 this.tablesDir_ = goog.isDef(opt_tablesDir) ? opt_tablesDir : null;
32 * Native Client <embed> element.
33 * {@code null} when no <embed> is attached to the DOM.
34 * @private {HTMLEmbedElement}
36 this.embedElement_ = null;
39 * Pending RPC callbacks. Maps from message IDs to callbacks.
40 * @private {!Object<function(!Object)>}
42 this.pendingRpcCallbacks_ = {};
45 * Next message ID to be used. Incremented with each sent message.
48 this.nextMessageId_ = 1;
53 * Set to {@code true} to enable debug logging of RPC messages.
56 cvox.LibLouis.DEBUG = false;
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.
64 cvox.LibLouis.prototype.attachToElement = function(elem) {
65 if (this.isAttached()) {
66 throw Error('Instance already attached');
69 var embed = document.createElement('embed');
70 embed.src = this.nmfPath_;
71 embed.type = 'application/x-nacl';
74 if (!goog.isNull(this.tablesDir_)) {
75 embed.setAttribute('tablesdir', this.tablesDir_);
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);
89 * Detaches the Native Client instance from the DOM.
91 cvox.LibLouis.prototype.detach = function() {
92 if (!this.isAttached()) {
93 throw Error('cannot detach unattached instance');
96 this.embedElement_.parentNode.removeChild(this.embedElement_);
97 this.embedElement_ = null;
98 for (var id in this.pendingRpcCallbacks_) {
99 this.pendingRpcCallbacks_[id]({});
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.
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
119 * @param {function(cvox.LibLouis.Translator)} callback
120 * Callback which will receive the translator, or {@code null} on failure.
122 cvox.LibLouis.prototype.getTranslator = function(tableNames, callback) {
123 if (!this.isAttached()) {
124 callback(null /* translator */);
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);
132 callback(null /* translator */);
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.
146 cvox.LibLouis.prototype.rpc_ =
147 function(command, message, callback) {
148 if (!this.isAttached()) {
149 throw Error('Cannot send RPC: liblouis instance not loaded');
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);
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.
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.
178 cvox.LibLouis.prototype.onInstanceError_ = function(e) {
179 window.console.error('failed to load liblouis Native Client instance');
185 * Invoked when the Native Client instance posts a message.
186 * @param {Event} e Event dispatched after the message was posted.
189 cvox.LibLouis.prototype.onInstanceMessage_ = function(e) {
190 if (cvox.LibLouis.DEBUG) {
191 window.console.debug('RPC <- ' + e.data);
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',
200 if (goog.isDef(message['error'])) {
201 window.console.error('liblouis Native Client error', message['error']);
203 var callback = this.pendingRpcCallbacks_[messageId];
204 if (goog.isDef(callback)) {
205 delete this.pendingRpcCallbacks_[messageId];
212 * Braille translator which uses a Native Client instance of liblouis.
214 * @param {!cvox.LibLouis} instance The instance wrapper.
215 * @param {string} tableNames Comma separated list of Table names to be passed
218 cvox.LibLouis.Translator = function(instance, tableNames) {
220 * The instance wrapper.
221 * @private {!cvox.LibLouis}
223 this.instance_ = instance;
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
242 cvox.LibLouis.Translator.prototype.translate = function(text, callback) {
243 if (!this.instance_.isAttached()) {
244 callback(null /*cells*/, null /*textToBraille*/, null /*brailleToText*/);
247 var message = { 'table_names': this.tableNames_, 'text': text };
248 this.instance_.rpc_('Translate', message, function(reply) {
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'];
257 if (goog.isDef(reply['braille_to_text'])) {
258 brailleToText = reply['braille_to_text'];
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));
265 callback(cells, textToBraille, brailleToText);
271 * Translates braille cells into text.
272 * @param {!ArrayBuffer} cells Cells to be translated.
273 * @param {function(?string)} callback Callback for result.
275 cvox.LibLouis.Translator.prototype.backTranslate =
276 function(cells, callback) {
277 if (!this.instance_.isAttached()) {
278 callback(null /*text*/);
281 if (cells.byteLength == 0) {
282 // liblouis doesn't handle empty input, so handle that trivially
288 'table_names': this.tableNames_,
289 'cells': cvox.LibLouis.Translator.encodeHexString_(cells)
291 this.instance_.rpc_('BackTranslate', message, function(reply) {
292 if (reply['success'] && goog.isString(reply['text'])) {
293 callback(reply['text']);
295 callback(null /* text */);
302 * Decodes a hexadecimal string to an {@code ArrayBuffer}.
303 * @param {string} hex Hexadecimal string.
304 * @return {!ArrayBuffer} Decoded binary data.
307 cvox.LibLouis.Translator.decodeHexString_ = function(hex) {
308 if (!/^([0-9a-f]{2})*$/i.test(hex)) {
309 throw Error('invalid hexadecimal string');
311 var array = new Uint8Array(hex.length / 2);
313 for (var i = 0; i < hex.length; i += 2) {
314 array[idx++] = parseInt(hex.substring(i, i + 2), 16);
321 * Encodes an {@code ArrayBuffer} in hexadecimal.
322 * @param {!ArrayBuffer} arrayBuffer Binary data.
323 * @return {string} Hexadecimal string.
326 cvox.LibLouis.Translator.encodeHexString_ = function(arrayBuffer) {
327 var array = new Uint8Array(arrayBuffer);
329 for (var i = 0; i < array.length; i++) {
331 hex += (b < 0x10 ? '0' : '') + b.toString(16);