Home | History | Annotate | Download | only in chrome
      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 
     10 goog.provide('cvox.ExpandingBrailleTranslator');
     11 
     12 goog.require('cvox.BrailleUtil');
     13 goog.require('cvox.LibLouis');
     14 goog.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  */
     34 cvox.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  */
     54 cvox.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  */
     87 cvox.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  */
    168 cvox.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  */
    198 cvox.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  */
    235 cvox.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  */
    291 cvox.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  */
    312 cvox.ExpandingBrailleTranslator.Range_;
    313