expanding_braille_translator.js revision 46d4c2bc3267f3f028f39e7e311b0f89aba2e4fd
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 Translates text to braille, optionally with some parts 7 * uncontracted. 8 */ 9 10goog.provide('cvox.ExpandingBrailleTranslator'); 11 12goog.require('cvox.BrailleUtil'); 13goog.require('cvox.LibLouis'); 14goog.require('cvox.Spannable'); 15 16 17/** 18 * A wrapper around one or two braille translators that uses contracted 19 * braille or not based on the selection start- and end-points (if any) in the 20 * translated text. If only one translator is provided, then that translator 21 * is used for all text regardless of selection. If two translators 22 * are provided, then the uncontracted translator is used for some text 23 * around the selection end-points and the contracted translator is used 24 * for all other text. When determining what text to use uncontracted 25 * translation for around a position, a region surrounding that position 26 * containing either only whitespace characters or only non-whitespace 27 * characters is used. 28 * @param {!cvox.LibLouis.Translator} defaultTranslator The translator for all 29 * text when the uncontracted translator is not used. 30 * @param {cvox.LibLouis.Translator=} opt_uncontractedTranslator 31 * Translator to use for uncontracted braille translation. 32 * @constructor 33 */ 34cvox.ExpandingBrailleTranslator = 35 function(defaultTranslator, opt_uncontractedTranslator) { 36 /** 37 * @type {!cvox.LibLouis.Translator} 38 * @private 39 */ 40 this.defaultTranslator_ = defaultTranslator; 41 /** 42 * @type {cvox.LibLouis.Translator} 43 * @private 44 */ 45 this.uncontractedTranslator_ = opt_uncontractedTranslator || null; 46}; 47 48 49/** 50 * What expansion to apply to the part of the translated string marked by the 51 * {@code cvox.BrailleUtil.ValueSpan} spannable. 52 * @enum {number} 53 */ 54cvox.ExpandingBrailleTranslator.ExpansionType = { 55 /** 56 * Use the default translator all of the value, regardless of any selection. 57 * This is typically used when the user is in the middle of typing and the 58 * typing started outside of a word. 59 */ 60 NONE: 0, 61 /** 62 * Expand text around the selection end-points if any. If the selection is 63 * a cursor, expand the text that occupies the positions right before and 64 * after the cursor. This is typically used when the user hasn't started 65 * typing contracted braille or when editing inside a word. 66 */ 67 SELECTION: 1, 68 /** 69 * Expand all text covered by the value span. this is typically used when 70 * the user is editing a text field where it doesn't make sense to use 71 * contracted braille (such as a url or email address). 72 */ 73 ALL: 2 74}; 75 76 77/** 78 * Translates text to braille using the translator(s) provided to the 79 * constructor. See {@code cvox.LibLouis.Translator} for further details. 80 * @param {!cvox.Spannable} text Text to translate. 81 * @param {cvox.ExpandingBrailleTranslator.ExpansionType} expansionType 82 * Indicates how the text marked by a value span, if any, is expanded. 83 * @param {function(!ArrayBuffer, !Array.<number>, !Array.<number>)} 84 * callback Called when the translation is done. It takes resulting 85 * braille cells and positional mappings as parameters. 86 */ 87cvox.ExpandingBrailleTranslator.prototype.translate = 88 function(text, expansionType, callback) { 89 var expandRanges = this.findExpandRanges_(text, expansionType); 90 if (expandRanges.length == 0) { 91 this.defaultTranslator_.translate( 92 text.toString(), 93 cvox.ExpandingBrailleTranslator.nullParamsToEmptyAdapter_( 94 text.getLength(), callback)); 95 return; 96 } 97 98 var chunks = []; 99 function addChunk(translator, start, end) { 100 chunks.push({translator: translator, start: start, end: end}); 101 } 102 var lastEnd = 0; 103 for (var i = 0; i < expandRanges.length; ++i) { 104 var range = expandRanges[i]; 105 if (lastEnd < range.start) { 106 addChunk(this.defaultTranslator_, lastEnd, range.start); 107 } 108 addChunk(this.uncontractedTranslator_, range.start, range.end); 109 lastEnd = range.end; 110 } 111 if (lastEnd < text.getLength()) { 112 addChunk(this.defaultTranslator_, lastEnd, text.getLength()); 113 } 114 115 var numPendingCallbacks = chunks.length; 116 117 function chunkTranslated(chunk, cells, textToBraille, brailleToText) { 118 chunk.cells = cells; 119 chunk.textToBraille = textToBraille; 120 chunk.brailleToText = brailleToText; 121 if (--numPendingCallbacks <= 0) { 122 finish(); 123 } 124 } 125 126 function finish() { 127 var totalCells = chunks.reduce( 128 function(accum, chunk) { return accum + chunk.cells.byteLength}, 0); 129 var cells = new Uint8Array(totalCells); 130 var cellPos = 0; 131 var textToBraille = []; 132 var brailleToText = []; 133 function appendAdjusted(array, toAppend, adjustment) { 134 array.push.apply(array, toAppend.map( 135 function(elem) { return adjustment + elem; } 136 )); 137 } 138 for (var i = 0, chunk; chunk = chunks[i]; ++i) { 139 cells.set(new Uint8Array(chunk.cells), cellPos); 140 appendAdjusted(textToBraille, chunk.textToBraille, cellPos); 141 appendAdjusted(brailleToText, chunk.brailleToText, chunk.start); 142 cellPos += chunk.cells.byteLength; 143 } 144 callback(cells.buffer, textToBraille, brailleToText); 145 } 146 147 for (var i = 0, chunk; chunk = chunks[i]; ++i) { 148 chunk.translator.translate( 149 text.toString().substring(chunk.start, chunk.end), 150 cvox.ExpandingBrailleTranslator.nullParamsToEmptyAdapter_( 151 chunk.end - chunk.start, goog.partial(chunkTranslated, chunk))); 152 } 153}; 154 155 156/** 157 * Expands a position to a range that covers the consecutive range of 158 * either whitespace or non whitespace characters around it. 159 * @param {string} str Text to look in. 160 * @param {number} pos Position to start looking at. 161 * @param {number} start Minimum value for the start position of the returned 162 * range. 163 * @param {number} end Maximum value for the end position of the returned 164 * range. 165 * @return {!cvox.ExpandingBrailleTranslator.Range_} The claculated range. 166 * @private 167 */ 168cvox.ExpandingBrailleTranslator.rangeForPosition_ = function( 169 str, pos, start, end) { 170 if (start < 0 || end > str.length) { 171 throw RangeError( 172 'End-points out of range looking for braille expansion range'); 173 } 174 if (pos < start || pos >= end) { 175 throw RangeError( 176 'Position out of range looking for braille expansion range'); 177 } 178 // Find the last chunk of either whitespace or non-whitespace before and 179 // including pos. 180 var start = str.substring(start, pos + 1).search(/(\s+|\S+)$/) + start; 181 // Find the characters to include after pos, starting at pos so that 182 // they are the same kind (either whitespace or not) as the 183 // characters starting at start. 184 var end = pos + /^(\s+|\S+)/.exec(str.substring(pos, end))[0].length; 185 return {start: start, end: end}; 186}; 187 188 189/** 190 * Finds the ranges in which contracted braille should not be used. 191 * @param {!cvox.Spannable} text Text to find expansion ranges in. 192 * @param {cvox.ExpandingBrailleTranslator.ExpansionType} expansionType 193 * Indicates how the text marked up as the value is expanded. 194 * @return {!Array.<cvox.ExpandingBrailleTranslator.Range_>} The calculated 195 * ranges. 196 * @private 197 */ 198cvox.ExpandingBrailleTranslator.prototype.findExpandRanges_ = function( 199 text, expansionType) { 200 var result = []; 201 if (this.uncontractedTranslator_ && 202 expansionType != cvox.ExpandingBrailleTranslator.ExpansionType.NONE) { 203 var value = text.getSpanInstanceOf(cvox.BrailleUtil.ValueSpan); 204 if (value) { 205 // The below type casts are valid because the ranges must be valid when 206 // the span is known to exist. 207 var valueStart = /** @type {number} */ (text.getSpanStart(value)); 208 var valueEnd = /** @type {number} */ (text.getSpanEnd(value)); 209 switch (expansionType) { 210 case cvox.ExpandingBrailleTranslator.ExpansionType.SELECTION: 211 this.addRangesForSelection_(text, valueStart, valueEnd, result); 212 break; 213 case cvox.ExpandingBrailleTranslator.ExpansionType.ALL: 214 result.push({start: valueStart, end: valueEnd}); 215 break; 216 } 217 } 218 } 219 220 return result; 221}; 222 223 224/** 225 * Finds ranges to expand around selection end points inside the value of 226 * a string. If any ranges are found, adds them to {@code outRanges}. 227 * @param {cvox.Spannable} text Text to find ranges in. 228 * @param {number} valueStart Start of the value in {@code text}. 229 * @param {number} valueEnd End of the value in {@code text}. 230 * @param {Array.<cvox.ExpandingBrailleTranslator.Range_>} outRanges 231 * Destination for the expansion ranges. Untouched if no ranges 232 * are found. Note that ranges may be coalesced. 233 * @private 234 */ 235cvox.ExpandingBrailleTranslator.prototype.addRangesForSelection_ = function( 236 text, valueStart, valueEnd, outRanges) { 237 var selection = text.getSpanInstanceOf( 238 cvox.BrailleUtil.ValueSelectionSpan); 239 if (!selection) { 240 return; 241 } 242 var selectionStart = text.getSpanStart(selection); 243 var selectionEnd = text.getSpanEnd(selection); 244 if (selectionStart < valueStart || selectionEnd > valueEnd) { 245 return; 246 } 247 var expandPositions = []; 248 if (selectionStart == valueEnd) { 249 if (selectionStart > valueStart) { 250 expandPositions.push(selectionStart - 1); 251 } 252 } else { 253 if (selectionStart == selectionEnd && selectionStart > valueStart) { 254 expandPositions.push(selectionStart - 1); 255 } 256 expandPositions.push(selectionStart); 257 // Include the selection end if the length of the selection is 258 // greater than one (otherwise this position would be redundant). 259 if (selectionEnd > selectionStart + 1) { 260 // Look at the last actual character of the selection, not the 261 // character at the (exclusive) end position. 262 expandPositions.push(selectionEnd - 1); 263 } 264 } 265 266 var lastRange = outRanges[outRanges.length - 1] || null; 267 for (var i = 0; i < expandPositions.length; ++i) { 268 var range = cvox.ExpandingBrailleTranslator.rangeForPosition_( 269 text.toString(), expandPositions[i], valueStart, valueEnd); 270 if (lastRange && lastRange.end >= range.start) { 271 lastRange.end = range.end; 272 } else { 273 outRanges.push(range); 274 lastRange = range; 275 } 276 } 277}; 278 279 280/** 281 * Adapts {@code callback} to accept null arguments and treat them as if the 282 * translation result is empty. 283 * @param {number} inputLength Length of the input to the translation. 284 * Used for populating {@code textToBraille} if null. 285 * @param {function(!ArrayBuffer, !Array.<number>, !Array.<number>)} callback 286 * The callback to adapt. 287 * @return {function(ArrayBuffer, Array.<number>, Array.<number>)} 288 * An adapted version of the callback. 289 * @private 290 */ 291cvox.ExpandingBrailleTranslator.nullParamsToEmptyAdapter_ = 292 function(inputLength, callback) { 293 return function(cells, textToBraille, brailleToText) { 294 if (!textToBraille) { 295 textToBraille = new Array(inputLength); 296 for (var i = 0; i < inputLength; ++i) { 297 textToBraille[i] = 0; 298 } 299 } 300 callback(cells || new ArrayBuffer(0), 301 textToBraille, 302 brailleToText || []); 303 }; 304}; 305 306 307/** 308 * A character range with inclusive start and exclusive end positions. 309 * @typedef {{start: number, end: number}} 310 * @private 311 */ 312cvox.ExpandingBrailleTranslator.Range_; 313