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_
= {};