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