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
});
73 throw new RangeError('span out of range (start=' + start
+
74 ', end=' + end
+ ', len=' + this.string_
.length
+ ')');
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);
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
) {
104 span
.start
+ originalLength
,
105 span
.end
+ originalLength
);
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
) {
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) {
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) {
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
);
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
) {
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
);
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
) {
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
) {
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
});
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.
273 cvox
.Spannable
.prototype.trim_ = function(trimStart
, trimEnd
) {
274 if (!trimStart
&& !trimEnd
) {
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() {
303 result
.string
= this.string_
;
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
309 var serializeInfo
= goog
.object
.findValue(
310 cvox
.Spannable
.serializableSpansByName_
,
311 function(v
) { return v
.ctor
=== span
.value
.constructor; });
313 var spanObj
= {type
: serializeInfo
.name
,
316 if (serializeInfo
.toJson
) {
317 spanObj
.value
= serializeInfo
.toJson
.apply(span
.value
);
319 result
.spans
.push(spanObj
);
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
);
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
377 * @param {!Function} constructor The type of the object that can be converted.
378 * This constructor will be called with no arguments to construct
380 * @param {string} name Name of the type used in the serializable object.
382 cvox
.Spannable
.registerStatelessSerializableSpan = function(
384 var obj
= {name
: name
, ctor
: constructor, toJson
: undefined};
386 * @param {!Object} obj
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)}}
403 cvox
.Spannable
.SerializeInfo_
;
407 * The serialized format of a spannable.
408 * @typedef {{string: string, spans: Array<cvox.Spannable.SerializedSpan_>}}
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}}
419 cvox
.Spannable
.SerializedSpan_
;
422 * Maps type names to serialization info objects.
423 * @type {Object<string, cvox.Spannable.SerializeInfo_>}
426 cvox
.Spannable
.serializableSpansByName_
= {};