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.
6 * @fileoverview Class which allows construction of annotated strings.
9 goog.provide('cvox.Spannable');
11 goog.require('goog.object');
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) {
24 this.string_ = opt_string instanceof cvox.Spannable ? '' : opt_string || '';
27 * Spans (annotations).
28 * @type {!Array<!{ value: *, start: number, end: number }>}
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 });
46 cvox.Spannable.prototype.toString = function() {
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;
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 this.spans_.sort(function(a, b) {
73 var ret = a.start - b.start;
79 throw new RangeError('span out of range (start=' + start +
80 ', end=' + end + ', len=' + this.string_.length + ')');
87 * @param {*} value Annotation.
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);
99 * Appends another Spannable or string to this one.
100 * @param {string|!cvox.Spannable} other String or spannable to concatenate.
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) {
110 span.start + originalLength,
111 span.end + originalLength);
113 } else if (typeof other === 'string') {
114 this.string_ += /** @type {string} */ (other);
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.
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) {
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.
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) {
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.
155 cvox.Spannable.prototype.getSpansInstanceOf = function(constructor) {
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);
168 * Returns all spans matching a position.
169 * @param {number} position Position to query.
170 * @return {!Array} Values annotating that position.
172 cvox.Spannable.prototype.getSpans = function(position) {
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);
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.
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) {
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.
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) {
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.
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');
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 });
248 * Trims whitespace from the beginning.
249 * @return {!cvox.Spannable} String with whitespace removed.
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.
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.
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.
281 cvox.Spannable.prototype.trim_ = function(trimStart, trimEnd) {
282 if (!trimStart && !trimEnd) {
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);
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.
309 cvox.Spannable.prototype.toJson = function() {
311 result.string = this.string_;
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
317 var serializeInfo = goog.object.findValue(
318 cvox.Spannable.serializableSpansByName_,
319 function(v) { return v.ctor === span.value.constructor; });
321 var spanObj = {type: serializeInfo.name,
324 if (serializeInfo.toJson) {
325 spanObj.value = serializeInfo.toJson.apply(span.value);
327 result.spans.push(spanObj);
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}
340 cvox.Spannable.fromJson = function(obj) {
341 if (typeof obj.string !== 'string') {
342 throw 'Invalid spannable json object: string field not a string';
344 if (!(obj.spans instanceof Array)) {
345 throw 'Invalid spannable json object: no spans array';
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';
352 if (typeof span.start !== 'number' || typeof span.end !== 'number') {
353 throw 'Invalid span in spannable json object: start or end not a number';
355 var serializeInfo = cvox.Spannable.serializableSpansByName_[span.type];
356 var value = serializeInfo.fromJson(span.value);
357 result.setSpan(value, span.start, span.end);
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.
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
385 * @param {!Function} constructor The type of the object that can be converted.
386 * This constructor will be called with no arguments to construct
388 * @param {string} name Name of the type used in the serializable object.
390 cvox.Spannable.registerStatelessSerializableSpan = function(
392 var obj = {name: name, ctor: constructor, toJson: undefined};
394 * @param {!Object} obj
397 obj.fromJson = function(obj) {
398 return new constructor();
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)}}
411 cvox.Spannable.SerializeInfo_;
415 * The serialized format of a spannable.
416 * @typedef {{string: string, spans: Array<cvox.Spannable.SerializedSpan_>}}
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}}
427 cvox.Spannable.SerializedSpan_;
430 * Maps type names to serialization info objects.
431 * @type {Object<cvox.Spannable.SerializeInfo_>}
434 cvox.Spannable.serializableSpansByName_ = {};