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