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<string, function(!Object)>}
42 this.pendingRpcCallbacks_ = {};
45 * Next message ID to be used. Incremented with each sent message.
48 this.nextMessageId_ = 1;
53 * Attaches the Native Client wrapper to the DOM as a child of the provided
54 * element, assumed to already be in the document.
55 * @param {!Element} elem Desired parent element of the instance.
57 cvox.LibLouis.prototype.attachToElement = function(elem) {
58 if (this.isAttached()) {
59 throw Error('Instance already attached');
62 var embed = document.createElement('embed');
63 embed.src = this.nmfPath_;
64 embed.type = 'application/x-nacl';
67 if (!goog.isNull(this.tablesDir_)) {
68 embed.setAttribute('tablesdir', this.tablesDir_);
70 embed.addEventListener('load', goog.bind(this.onInstanceLoad_, this),
71 false /* useCapture */);
72 embed.addEventListener('error', goog.bind(this.onInstanceError_, this),
73 false /* useCapture */);
74 embed.addEventListener('message', goog.bind(this.onInstanceMessage_, this),
75 false /* useCapture */);
76 elem.appendChild(embed);
77 this.embedElement_ = /** @type {!HTMLEmbedElement} */ (embed);
82 * Detaches the Native Client instance from the DOM.
84 cvox.LibLouis.prototype.detach = function() {
85 if (!this.isAttached()) {
86 throw Error('cannot detach unattached instance');
89 this.embedElement_.parentNode.removeChild(this.embedElement_);
90 this.embedElement_ = null;
91 for (var id in this.pendingRpcCallbacks_) {
92 this.pendingRpcCallbacks_[id]({});
94 this.pendingRpcCallbacks_ = {};
99 * Determines whether the Native Client instance is attached.
100 * @return {boolean} {@code true} if the <embed> element is attached to the DOM.
102 cvox.LibLouis.prototype.isAttached = function() {
103 return this.embedElement_ !== null;
108 * Returns a translator for the desired table, asynchronously.
109 * This object must be attached to a document when requesting a translator.
110 * @param {string} tableNames Comma separated list of braille table names for
112 * @param {function(cvox.LibLouis.Translator)} callback
113 * Callback which will receive the translator, or {@code null} on failure.
115 cvox.LibLouis.prototype.getTranslator = function(tableNames, callback) {
116 if (!this.isAttached()) {
117 callback(null /* translator */);
120 this.rpc_('CheckTable', { 'table_names': tableNames }, function(reply) {
121 if (reply['success']) {
122 var translator = new cvox.LibLouis.Translator(this, tableNames);
123 callback(translator);
125 callback(null /* translator */);
132 * Dispatches a message to the remote end and returns the reply asynchronously.
133 * A message ID will be automatically assigned (as a side-effect).
134 * @param {string} command Command name to be sent.
135 * @param {!Object} message JSONable message to be sent.
136 * @param {function(!Object)} callback Callback to receive the reply.
139 cvox.LibLouis.prototype.rpc_ =
140 function(command, message, callback) {
141 if (!this.isAttached()) {
142 throw Error('Cannot send RPC: liblouis instance not loaded');
144 var messageId = '' + this.nextMessageId_++;
145 message['message_id'] = messageId;
146 message['command'] = command;
147 var json = JSON.stringify(message);
149 window.console.debug('RPC -> ' + json);
151 this.embedElement_.postMessage(json);
152 this.pendingRpcCallbacks_[messageId] = callback;
157 * Invoked when the Native Client instance successfully loads.
158 * @param {Event} e Event dispatched after loading.
161 cvox.LibLouis.prototype.onInstanceLoad_ = function(e) {
162 window.console.info('loaded liblouis Native Client instance');
167 * Invoked when the Native Client instance fails to load.
168 * @param {Event} e Event dispatched after loading failure.
171 cvox.LibLouis.prototype.onInstanceError_ = function(e) {
172 window.console.error('failed to load liblouis Native Client instance');
178 * Invoked when the Native Client instance posts a message.
179 * @param {Event} e Event dispatched after the message was posted.
182 cvox.LibLouis.prototype.onInstanceMessage_ = function(e) {
184 window.console.debug('RPC <- ' + e.data);
186 var message = /** @type {!Object} */ (JSON.parse(e.data));
187 var messageId = message['in_reply_to'];
188 if (!goog.isDef(messageId)) {
189 window.console.warn('liblouis Native Client module sent message with no ID',
193 if (goog.isDef(message['error'])) {
194 window.console.error('liblouis Native Client error', message['error']);
196 var callback = this.pendingRpcCallbacks_[messageId];
197 if (goog.isDef(callback)) {
198 delete this.pendingRpcCallbacks_[messageId];
205 * Braille translator which uses a Native Client instance of liblouis.
207 * @param {!cvox.LibLouis} instance The instance wrapper.
208 * @param {string} tableNames Comma separated list of Table names to be passed
211 cvox.LibLouis.Translator = function(instance, tableNames) {
213 * The instance wrapper.
214 * @private {!cvox.LibLouis}
216 this.instance_ = instance;
222 this.tableNames_ = tableNames;
227 * Translates text into braille cells.
228 * @param {string} text Text to be translated.
229 * @param {function(ArrayBuffer, Array<number>, Array<number>)} callback
230 * Callback for result. Takes 3 parameters: the resulting cells,
231 * mapping from text to braille positions and mapping from braille to
232 * text positions. If translation fails for any reason, all parameters are
235 cvox.LibLouis.Translator.prototype.translate = function(text, callback) {
236 if (!this.instance_.isAttached()) {
237 callback(null /*cells*/, null /*textToBraille*/, null /*brailleToText*/);
240 var message = { 'table_names': this.tableNames_, 'text': text };
241 this.instance_.rpc_('Translate', message, function(reply) {
243 var textToBraille = null;
244 var brailleToText = null;
245 if (reply['success'] && goog.isString(reply['cells'])) {
246 cells = cvox.LibLouis.Translator.decodeHexString_(reply['cells']);
247 if (goog.isDef(reply['text_to_braille'])) {
248 textToBraille = reply['text_to_braille'];
250 if (goog.isDef(reply['braille_to_text'])) {
251 brailleToText = reply['braille_to_text'];
253 } else if (text.length > 0) {
254 // TODO(plundblad): The nacl wrapper currently returns an error
255 // when translating an empty string. Address that and always log here.
256 console.error('Braille translation error for ' + JSON.stringify(message));
258 callback(cells, textToBraille, brailleToText);
264 * Translates braille cells into text.
265 * @param {!ArrayBuffer} cells Cells to be translated.
266 * @param {function(?string)} callback Callback for result.
268 cvox.LibLouis.Translator.prototype.backTranslate =
269 function(cells, callback) {
270 if (!this.instance_.isAttached()) {
271 callback(null /*text*/);
274 if (cells.byteLength == 0) {
275 // liblouis doesn't handle empty input, so handle that trivially
281 'table_names': this.tableNames_,
282 'cells': cvox.LibLouis.Translator.encodeHexString_(cells)
284 this.instance_.rpc_('BackTranslate', message, function(reply) {
285 if (reply['success'] && goog.isString(reply['text'])) {
286 callback(reply['text']);
288 callback(null /* text */);
295 * Decodes a hexadecimal string to an {@code ArrayBuffer}.
296 * @param {string} hex Hexadecimal string.
297 * @return {!ArrayBuffer} Decoded binary data.
300 cvox.LibLouis.Translator.decodeHexString_ = function(hex) {
301 if (!/^([0-9a-f]{2})*$/i.test(hex)) {
302 throw Error('invalid hexadecimal string');
304 var array = new Uint8Array(hex.length / 2);
306 for (var i = 0; i < hex.length; i += 2) {
307 array[idx++] = parseInt(hex.substring(i, i + 2), 16);
314 * Encodes an {@code ArrayBuffer} in hexadecimal.
315 * @param {!ArrayBuffer} arrayBuffer Binary data.
316 * @return {string} Hexadecimal string.
319 cvox.LibLouis.Translator.encodeHexString_ = function(arrayBuffer) {
320 var array = new Uint8Array(arrayBuffer);
322 for (var i = 0; i < array.length; i++) {
324 hex += (b < 0x10 ? '0' : '') + b.toString(16);