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