Add new certificateProvider extension API.
[chromium-blink-merge.git] / chrome / browser / resources / chromeos / chromevox / common / spannable.js
blobbe77ba04ae71b68d8bea1acdd731760d82166500
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 Class which allows construction of annotated strings.
7  */
9 goog.provide('cvox.Spannable');
11 goog.require('goog.object');
13 /**
14  * @constructor
15  * @param {string|!cvox.Spannable=} opt_string Initial value of the spannable.
16  * @param {*=} opt_annotation Initial annotation for the entire string.
17  */
18 cvox.Spannable = function(opt_string, opt_annotation) {
19   /**
20    * Underlying string.
21    * @type {string}
22    * @private
23    */
24   this.string_ = opt_string instanceof cvox.Spannable ? '' : opt_string || '';
26   /**
27    * Spans (annotations).
28    * @type {!Array<!{ value: *, start: number, end: number }>}
29    * @private
30    */
31   this.spans_ = [];
33   // Append the initial spannable.
34   if (opt_string instanceof cvox.Spannable)
35   this.append(opt_string);
37   // Optionally annotate the entire string.
38   if (goog.isDef(opt_annotation)) {
39     var len = this.string_.length;
40     this.spans_.push({ value: opt_annotation, start: 0, end: len });
41   }
45 /** @override */
46 cvox.Spannable.prototype.toString = function() {
47   return this.string_;
51 /**
52  * Returns the length of the string.
53  * @return {number} Length of the string.
54  */
55 cvox.Spannable.prototype.getLength = function() {
56   return this.string_.length;
60 /**
61  * Adds a span to some region of the string.
62  * @param {*} value Annotation.
63  * @param {number} start Starting index (inclusive).
64  * @param {number} end Ending index (exclusive).
65  */
66 cvox.Spannable.prototype.setSpan = function(value, start, end) {
67   this.removeSpan(value);
68   if (0 <= start && start <= end && end <= this.string_.length) {
69     // Zero-length spans are explicitly allowed, because it is possible to
70     // query for position by annotation as well as the reverse.
71     this.spans_.push({ value: value, start: start, end: end });
72     this.spans_.sort(function(a, b) {
73       var ret = a.start - b.start;
74       if (ret == 0)
75         ret = a.end - b.end;
76       return ret;
77     });
78   } else {
79     throw new RangeError('span out of range (start=' + start +
80         ', end=' + end + ', len=' + this.string_.length + ')');
81   }
85 /**
86  * Removes a span.
87  * @param {*} value Annotation.
88  */
89 cvox.Spannable.prototype.removeSpan = function(value) {
90   for (var i = this.spans_.length - 1; i >= 0; i--) {
91     if (this.spans_[i].value === value) {
92       this.spans_.splice(i, 1);
93     }
94   }
98 /**
99  * Appends another Spannable or string to this one.
100  * @param {string|!cvox.Spannable} other String or spannable to concatenate.
101  */
102 cvox.Spannable.prototype.append = function(other) {
103   if (other instanceof cvox.Spannable) {
104     var otherSpannable = /** @type {!cvox.Spannable} */ (other);
105     var originalLength = this.getLength();
106     this.string_ += otherSpannable.string_;
107     other.spans_.forEach(goog.bind(function(span) {
108       this.setSpan(
109           span.value,
110           span.start + originalLength,
111           span.end + originalLength);
112     }, this));
113   } else if (typeof other === 'string') {
114     this.string_ += /** @type {string} */ (other);
115   }
120  * Returns the first value matching a position.
121  * @param {number} position Position to query.
122  * @return {*} Value annotating that position, or undefined if none is found.
123  */
124 cvox.Spannable.prototype.getSpan = function(position) {
125   for (var i = 0; i < this.spans_.length; i++) {
126     var span = this.spans_[i];
127     if (span.start <= position && position < span.end) {
128       return span.value;
129     }
130   }
135  * Returns the first span value which is an instance of a given constructor.
136  * @param {!Function} constructor Constructor.
137  * @return {!Object|undefined} Object if found; undefined otherwise.
138  */
139 cvox.Spannable.prototype.getSpanInstanceOf = function(constructor) {
140   for (var i = 0; i < this.spans_.length; i++) {
141     var span = this.spans_[i];
142     if (span.value instanceof constructor) {
143       return span.value;
144     }
145   }
149  * Returns all span values which are an instance of a given constructor.
150  * Spans are returned in the order of their starting index and ending index
151  * for spans with equals tarting indices.
152  * @param {!Function} constructor Constructor.
153  * @return {!Array<Object>} Array of object.
154  */
155 cvox.Spannable.prototype.getSpansInstanceOf = function(constructor) {
156   var ret = [];
157   for (var i = 0; i < this.spans_.length; i++) {
158     var span = this.spans_[i];
159     if (span.value instanceof constructor) {
160       ret.push(span.value);
161     }
162   }
163   return ret;
168  * Returns all spans matching a position.
169  * @param {number} position Position to query.
170  * @return {!Array} Values annotating that position.
171  */
172 cvox.Spannable.prototype.getSpans = function(position) {
173   var results = [];
174   for (var i = 0; i < this.spans_.length; i++) {
175     var span = this.spans_[i];
176     if (span.start <= position && position < span.end) {
177       results.push(span.value);
178     }
179   }
180   return results;
185  * Returns the start of the requested span.
186  * @param {*} value Annotation.
187  * @return {number|undefined} Start of the span, or undefined if not attached.
188  */
189 cvox.Spannable.prototype.getSpanStart = function(value) {
190   for (var i = 0; i < this.spans_.length; i++) {
191     var span = this.spans_[i];
192     if (span.value === value) {
193       return span.start;
194     }
195   }
196   return undefined;
201  * Returns the end of the requested span.
202  * @param {*} value Annotation.
203  * @return {number|undefined} End of the span, or undefined if not attached.
204  */
205 cvox.Spannable.prototype.getSpanEnd = function(value) {
206   for (var i = 0; i < this.spans_.length; i++) {
207     var span = this.spans_[i];
208     if (span.value === value) {
209       return span.end;
210     }
211   }
212   return undefined;
217  * Returns a substring of this spannable.
218  * Note that while similar to String#substring, this function is much less
219  * permissive about its arguments. It does not accept arguments in the wrong
220  * order or out of bounds.
222  * @param {number} start Start index, inclusive.
223  * @param {number=} opt_end End index, exclusive.
224  *     If excluded, the length of the string is used instead.
225  * @return {!cvox.Spannable} Substring requested.
226  */
227 cvox.Spannable.prototype.substring = function(start, opt_end) {
228   var end = goog.isDef(opt_end) ? opt_end : this.string_.length;
230   if (start < 0 || end > this.string_.length || start > end) {
231     throw new RangeError('substring indices out of range');
232   }
234   var result = new cvox.Spannable(this.string_.substring(start, end));
235   for (var i = 0; i < this.spans_.length; i++) {
236     var span = this.spans_[i];
237     if (span.start <= end && span.end >= start) {
238       var newStart = Math.max(0, span.start - start);
239       var newEnd = Math.min(end - start, span.end - start);
240       result.spans_.push({ value: span.value, start: newStart, end: newEnd });
241     }
242   }
243   return result;
248  * Trims whitespace from the beginning.
249  * @return {!cvox.Spannable} String with whitespace removed.
250  */
251 cvox.Spannable.prototype.trimLeft = function() {
252   return this.trim_(true, false);
257  * Trims whitespace from the end.
258  * @return {!cvox.Spannable} String with whitespace removed.
259  */
260 cvox.Spannable.prototype.trimRight = function() {
261   return this.trim_(false, true);
266  * Trims whitespace from the beginning and end.
267  * @return {!cvox.Spannable} String with whitespace removed.
268  */
269 cvox.Spannable.prototype.trim = function() {
270   return this.trim_(true, true);
275  * Trims whitespace from either the beginning and end or both.
276  * @param {boolean} trimStart Trims whitespace from the start of a string.
277  * @param {boolean} trimEnd Trims whitespace from the end of a string.
278  * @return {!cvox.Spannable} String with whitespace removed.
279  * @private
280  */
281 cvox.Spannable.prototype.trim_ = function(trimStart, trimEnd) {
282   if (!trimStart && !trimEnd) {
283     return this;
284   }
286   // Special-case whitespace-only strings, including the empty string.
287   // As an arbitrary decision, we treat this as trimming the whitespace off the
288   // end, rather than the beginning, of the string.
289   // This choice affects which spans are kept.
290   if (/^\s*$/.test(this.string_)) {
291     return this.substring(0, 0);
292   }
294   // Otherwise, we have at least one non-whitespace character to use as an
295   // anchor when trimming.
296   var trimmedStart = trimStart ? this.string_.match(/^\s*/)[0].length : 0;
297   var trimmedEnd = trimEnd ?
298       this.string_.match(/\s*$/).index : this.string_.length;
299   return this.substring(trimmedStart, trimmedEnd);
304  * Returns this spannable to a json serializable form, including the text and
305  * span objects whose types have been registered with registerSerializableSpan
306  * or registerStatelessSerializableSpan.
307  * @return {!cvox.Spannable.SerializedSpannable_} the json serializable form.
308  */
309 cvox.Spannable.prototype.toJson = function() {
310   var result = {};
311   result.string = this.string_;
312   result.spans = [];
313   for (var i = 0; i < this.spans_.length; ++i) {
314     var span = this.spans_[i];
315     // Use linear search, since using functions as property keys
316     // is not reliable.
317     var serializeInfo = goog.object.findValue(
318         cvox.Spannable.serializableSpansByName_,
319         function(v) { return v.ctor === span.value.constructor; });
320     if (serializeInfo) {
321       var spanObj = {type: serializeInfo.name,
322                      start: span.start,
323                      end: span.end};
324       if (serializeInfo.toJson) {
325         spanObj.value = serializeInfo.toJson.apply(span.value);
326       }
327       result.spans.push(spanObj);
328     }
329   }
330   return result;
335  * Creates a spannable from a json serializable representation.
336  * @param {!cvox.Spannable.SerializedSpannable_} obj object containing the
337  *     serializable representation.
338  * @return {!cvox.Spannable}
339  */
340 cvox.Spannable.fromJson = function(obj) {
341   if (typeof obj.string !== 'string') {
342     throw 'Invalid spannable json object: string field not a string';
343   }
344   if (!(obj.spans instanceof Array)) {
345     throw 'Invalid spannable json object: no spans array';
346   }
347   var result = new cvox.Spannable(obj.string);
348   for (var i = 0, span; span = obj.spans[i]; ++i) {
349     if (typeof span.type !== 'string') {
350       throw 'Invalid span in spannable json object: type not a string';
351     }
352     if (typeof span.start !== 'number' || typeof span.end !== 'number') {
353       throw 'Invalid span in spannable json object: start or end not a number';
354     }
355     var serializeInfo = cvox.Spannable.serializableSpansByName_[span.type];
356     var value = serializeInfo.fromJson(span.value);
357     result.setSpan(value, span.start, span.end);
358   }
359   return result;
364  * Registers a type that can be converted to a json serializable format.
365  * @param {!Function} constructor The type of object that can be converted.
366  * @param {string} name String identifier used in the serializable format.
367  * @param {function(!Object): !Object} fromJson A function that converts
368  *     the serializable object to an actual object of this type.
369  * @param {function(!Object): !Object} toJson A function that converts
370  *     this object to a json serializable object.  The function will
371  *     be called with this set to the object to convert.
372  */
373 cvox.Spannable.registerSerializableSpan = function(
374     constructor, name, fromJson, toJson) {
375   var obj = {name: name, ctor: constructor,
376              fromJson: fromJson, toJson: toJson};
377   cvox.Spannable.serializableSpansByName_[name] = obj;
382  * Registers an object type that can be converted to/from a json serializable
383  * form.  Objects of this type carry no state that will be preserved
384  * when serialized.
385  * @param {!Function} constructor The type of the object that can be converted.
386  *     This constructor will be called with no arguments to construct
387  *     new objects.
388  * @param {string} name Name of the type used in the serializable object.
389  */
390 cvox.Spannable.registerStatelessSerializableSpan = function(
391     constructor, name) {
392   var obj = {name: name, ctor: constructor, toJson: undefined};
393   /**
394    * @param {!Object} obj
395    * @return {!Object}
396    */
397   obj.fromJson = function(obj) {
398      return new constructor();
399   };
400   cvox.Spannable.serializableSpansByName_[name] = obj;
405  * Describes how to convert a span type to/from serializable json.
406  * @typedef {{ctor: !Function, name: string,
407  *             fromJson: function(!Object): !Object,
408  *             toJson: ((function(!Object): !Object)|undefined)}}
409  * @private
410  */
411 cvox.Spannable.SerializeInfo_;
415  * The serialized format of a spannable.
416  * @typedef {{string: string, spans: Array<cvox.Spannable.SerializedSpan_>}}
417  * @private
418  */
419 cvox.Spannable.SerializedSpannable_;
423  * The format of a single annotation in a serialized spannable.
424  * @typedef {{type: string, value: !Object, start: number, end: number}}
425  * @private
426  */
427 cvox.Spannable.SerializedSpan_;
430  * Maps type names to serialization info objects.
431  * @type {Object<cvox.Spannable.SerializeInfo_>}
432  * @private
433  */
434 cvox.Spannable.serializableSpansByName_ = {};