[Extensions] Make extension message bubble factory platform-abstract
[chromium-blink-merge.git] / chrome / browser / resources / chromeos / chromevox / common / spannable.js
blobb4773e274a48f3a55a4f835ef1d8a88068d72180
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.
18 cvox.Spannable = function(opt_string, opt_annotation) {
19 /**
20 * Underlying string.
21 * @type {string}
22 * @private
24 this.string_ = opt_string instanceof cvox.Spannable ? '' : opt_string || '';
26 /**
27 * Spans (annotations).
28 * @type {!Array<!{ value: *, start: number, end: number }>}
29 * @private
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 });
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.
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).
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 } else {
73 throw new RangeError('span out of range (start=' + start +
74 ', end=' + end + ', len=' + this.string_.length + ')');
79 /**
80 * Removes a span.
81 * @param {*} value Annotation.
83 cvox.Spannable.prototype.removeSpan = function(value) {
84 for (var i = this.spans_.length - 1; i >= 0; i--) {
85 if (this.spans_[i].value === value) {
86 this.spans_.splice(i, 1);
92 /**
93 * Appends another Spannable or string to this one.
94 * @param {string|!cvox.Spannable} other String or spannable to concatenate.
96 cvox.Spannable.prototype.append = function(other) {
97 if (other instanceof cvox.Spannable) {
98 var otherSpannable = /** @type {!cvox.Spannable} */ (other);
99 var originalLength = this.getLength();
100 this.string_ += otherSpannable.string_;
101 other.spans_.forEach(goog.bind(function(span) {
102 this.setSpan(
103 span.value,
104 span.start + originalLength,
105 span.end + originalLength);
106 }, this));
107 } else if (typeof other === 'string') {
108 this.string_ += /** @type {string} */ (other);
114 * Returns the first value matching a position.
115 * @param {number} position Position to query.
116 * @return {*} Value annotating that position, or undefined if none is found.
118 cvox.Spannable.prototype.getSpan = function(position) {
119 for (var i = 0; i < this.spans_.length; i++) {
120 var span = this.spans_[i];
121 if (span.start <= position && position < span.end) {
122 return span.value;
129 * Returns the first span value which is an instance of a given constructor.
130 * @param {!Function} constructor Constructor.
131 * @return {!Object|undefined} Object if found; undefined otherwise.
133 cvox.Spannable.prototype.getSpanInstanceOf = function(constructor) {
134 for (var i = 0; i < this.spans_.length; i++) {
135 var span = this.spans_[i];
136 if (span.value instanceof constructor) {
137 return span.value;
143 * Returns all span values which are an instance of a given constructor.
144 * @param {!Function} constructor Constructor.
145 * @return {!Array<Object>} Array of object.
147 cvox.Spannable.prototype.getSpansInstanceOf = function(constructor) {
148 var ret = [];
149 for (var i = 0; i < this.spans_.length; i++) {
150 var span = this.spans_[i];
151 if (span.value instanceof constructor) {
152 ret.push(span.value);
155 return ret;
160 * Returns all spans matching a position.
161 * @param {number} position Position to query.
162 * @return {!Array} Values annotating that position.
164 cvox.Spannable.prototype.getSpans = function(position) {
165 var results = [];
166 for (var i = 0; i < this.spans_.length; i++) {
167 var span = this.spans_[i];
168 if (span.start <= position && position < span.end) {
169 results.push(span.value);
172 return results;
177 * Returns the start of the requested span.
178 * @param {*} value Annotation.
179 * @return {number|undefined} Start of the span, or undefined if not attached.
181 cvox.Spannable.prototype.getSpanStart = function(value) {
182 for (var i = 0; i < this.spans_.length; i++) {
183 var span = this.spans_[i];
184 if (span.value === value) {
185 return span.start;
188 return undefined;
193 * Returns the end of the requested span.
194 * @param {*} value Annotation.
195 * @return {number|undefined} End of the span, or undefined if not attached.
197 cvox.Spannable.prototype.getSpanEnd = function(value) {
198 for (var i = 0; i < this.spans_.length; i++) {
199 var span = this.spans_[i];
200 if (span.value === value) {
201 return span.end;
204 return undefined;
209 * Returns a substring of this spannable.
210 * Note that while similar to String#substring, this function is much less
211 * permissive about its arguments. It does not accept arguments in the wrong
212 * order or out of bounds.
214 * @param {number} start Start index, inclusive.
215 * @param {number=} opt_end End index, exclusive.
216 * If excluded, the length of the string is used instead.
217 * @return {!cvox.Spannable} Substring requested.
219 cvox.Spannable.prototype.substring = function(start, opt_end) {
220 var end = goog.isDef(opt_end) ? opt_end : this.string_.length;
222 if (start < 0 || end > this.string_.length || start > end) {
223 throw new RangeError('substring indices out of range');
226 var result = new cvox.Spannable(this.string_.substring(start, end));
227 for (var i = 0; i < this.spans_.length; i++) {
228 var span = this.spans_[i];
229 if (span.start <= end && span.end >= start) {
230 var newStart = Math.max(0, span.start - start);
231 var newEnd = Math.min(end - start, span.end - start);
232 result.spans_.push({ value: span.value, start: newStart, end: newEnd });
235 return result;
240 * Trims whitespace from the beginning.
241 * @return {!cvox.Spannable} String with whitespace removed.
243 cvox.Spannable.prototype.trimLeft = function() {
244 return this.trim_(true, false);
249 * Trims whitespace from the end.
250 * @return {!cvox.Spannable} String with whitespace removed.
252 cvox.Spannable.prototype.trimRight = function() {
253 return this.trim_(false, true);
258 * Trims whitespace from the beginning and end.
259 * @return {!cvox.Spannable} String with whitespace removed.
261 cvox.Spannable.prototype.trim = function() {
262 return this.trim_(true, true);
267 * Trims whitespace from either the beginning and end or both.
268 * @param {boolean} trimStart Trims whitespace from the start of a string.
269 * @param {boolean} trimEnd Trims whitespace from the end of a string.
270 * @return {!cvox.Spannable} String with whitespace removed.
271 * @private
273 cvox.Spannable.prototype.trim_ = function(trimStart, trimEnd) {
274 if (!trimStart && !trimEnd) {
275 return this;
278 // Special-case whitespace-only strings, including the empty string.
279 // As an arbitrary decision, we treat this as trimming the whitespace off the
280 // end, rather than the beginning, of the string.
281 // This choice affects which spans are kept.
282 if (/^\s*$/.test(this.string_)) {
283 return this.substring(0, 0);
286 // Otherwise, we have at least one non-whitespace character to use as an
287 // anchor when trimming.
288 var trimmedStart = trimStart ? this.string_.match(/^\s*/)[0].length : 0;
289 var trimmedEnd = trimEnd ?
290 this.string_.match(/\s*$/).index : this.string_.length;
291 return this.substring(trimmedStart, trimmedEnd);
296 * Returns this spannable to a json serializable form, including the text and
297 * span objects whose types have been registered with registerSerializableSpan
298 * or registerStatelessSerializableSpan.
299 * @return {!cvox.Spannable.SerializedSpannable_} the json serializable form.
301 cvox.Spannable.prototype.toJson = function() {
302 var result = {};
303 result.string = this.string_;
304 result.spans = [];
305 for (var i = 0; i < this.spans_.length; ++i) {
306 var span = this.spans_[i];
307 // Use linear search, since using functions as property keys
308 // is not reliable.
309 var serializeInfo = goog.object.findValue(
310 cvox.Spannable.serializableSpansByName_,
311 function(v) { return v.ctor === span.value.constructor; });
312 if (serializeInfo) {
313 var spanObj = {type: serializeInfo.name,
314 start: span.start,
315 end: span.end};
316 if (serializeInfo.toJson) {
317 spanObj.value = serializeInfo.toJson.apply(span.value);
319 result.spans.push(spanObj);
322 return result;
327 * Creates a spannable from a json serializable representation.
328 * @param {!cvox.Spannable.SerializedSpannable_} obj object containing the
329 * serializable representation.
330 * @return {!cvox.Spannable}
332 cvox.Spannable.fromJson = function(obj) {
333 if (typeof obj.string !== 'string') {
334 throw 'Invalid spannable json object: string field not a string';
336 if (!(obj.spans instanceof Array)) {
337 throw 'Invalid spannable json object: no spans array';
339 var result = new cvox.Spannable(obj.string);
340 for (var i = 0, span; span = obj.spans[i]; ++i) {
341 if (typeof span.type !== 'string') {
342 throw 'Invalid span in spannable json object: type not a string';
344 if (typeof span.start !== 'number' || typeof span.end !== 'number') {
345 throw 'Invalid span in spannable json object: start or end not a number';
347 var serializeInfo = cvox.Spannable.serializableSpansByName_[span.type];
348 var value = serializeInfo.fromJson(span.value);
349 result.setSpan(value, span.start, span.end);
351 return result;
356 * Registers a type that can be converted to a json serializable format.
357 * @param {!Function} constructor The type of object that can be converted.
358 * @param {string} name String identifier used in the serializable format.
359 * @param {function(!Object): !Object} fromJson A function that converts
360 * the serializable object to an actual object of this type.
361 * @param {function(!Object): !Object} toJson A function that converts
362 * this object to a json serializable object. The function will
363 * be called with this set to the object to convert.
365 cvox.Spannable.registerSerializableSpan = function(
366 constructor, name, fromJson, toJson) {
367 var obj = {name: name, ctor: constructor,
368 fromJson: fromJson, toJson: toJson};
369 cvox.Spannable.serializableSpansByName_[name] = obj;
374 * Registers an object type that can be converted to/from a json serializable
375 * form. Objects of this type carry no state that will be preserved
376 * when serialized.
377 * @param {!Function} constructor The type of the object that can be converted.
378 * This constructor will be called with no arguments to construct
379 * new objects.
380 * @param {string} name Name of the type used in the serializable object.
382 cvox.Spannable.registerStatelessSerializableSpan = function(
383 constructor, name) {
384 var obj = {name: name, ctor: constructor, toJson: undefined};
386 * @param {!Object} obj
387 * @return {!Object}
389 obj.fromJson = function(obj) {
390 return new constructor();
392 cvox.Spannable.serializableSpansByName_[name] = obj;
397 * Describes how to convert a span type to/from serializable json.
398 * @typedef {{ctor: !Function, name: string,
399 * fromJson: function(!Object): !Object,
400 * toJson: ((function(!Object): !Object)|undefined)}}
401 * @private
403 cvox.Spannable.SerializeInfo_;
407 * The serialized format of a spannable.
408 * @typedef {{string: string, spans: Array<cvox.Spannable.SerializedSpan_>}}
409 * @private
411 cvox.Spannable.SerializedSpannable_;
415 * The format of a single annotation in a serialized spannable.
416 * @typedef {{type: string, value: !Object, start: number, end: number}}
417 * @private
419 cvox.Spannable.SerializedSpan_;
422 * Maps type names to serialization info objects.
423 * @type {Object<string, cvox.Spannable.SerializeInfo_>}
424 * @private
426 cvox.Spannable.serializableSpansByName_ = {};