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);