OLD | NEW |
1 // Copyright 2014 The Chromium Authors. All rights reserved. | 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 | 2 // Use of this source code is governed by a BSD-style license that can be |
3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
4 | 4 |
5 /** | 5 /** |
6 * @fileoverview Class which allows construction of annotated strings. | 6 * @fileoverview Class which allows construction of annotated strings. |
7 */ | 7 */ |
8 | 8 |
9 goog.provide('Spannable'); | 9 goog.provide('Spannable'); |
10 | 10 |
(...skipping 19 matching lines...) Expand all Loading... |
30 */ | 30 */ |
31 this.spans_ = []; | 31 this.spans_ = []; |
32 | 32 |
33 // Append the initial spannable. | 33 // Append the initial spannable. |
34 if (opt_string instanceof Spannable) | 34 if (opt_string instanceof Spannable) |
35 this.append(opt_string); | 35 this.append(opt_string); |
36 | 36 |
37 // Optionally annotate the entire string. | 37 // Optionally annotate the entire string. |
38 if (goog.isDef(opt_annotation)) { | 38 if (goog.isDef(opt_annotation)) { |
39 var len = this.string_.length; | 39 var len = this.string_.length; |
40 this.spans_.push({ value: opt_annotation, start: 0, end: len }); | 40 this.spans_.push({value: opt_annotation, start: 0, end: len}); |
41 } | 41 } |
42 }; | 42 }; |
43 | 43 |
44 Spannable.prototype = { | 44 Spannable.prototype = { |
45 /** @override */ | 45 /** @override */ |
46 toString: function() { | 46 toString: function() { |
47 return this.string_; | 47 return this.string_; |
48 }, | 48 }, |
49 | 49 |
50 /** @return {number} The length of the string */ | 50 /** @return {number} The length of the string */ |
51 get length() { | 51 get length() { |
52 return this.string_.length; | 52 return this.string_.length; |
53 }, | 53 }, |
54 | 54 |
55 /** | 55 /** |
56 * Adds a span to some region of the string. | 56 * Adds a span to some region of the string. |
57 * @param {*} value Annotation. | 57 * @param {*} value Annotation. |
58 * @param {number} start Starting index (inclusive). | 58 * @param {number} start Starting index (inclusive). |
59 * @param {number} end Ending index (exclusive). | 59 * @param {number} end Ending index (exclusive). |
60 */ | 60 */ |
61 setSpan: function(value, start, end) { | 61 setSpan: function(value, start, end) { |
62 this.removeSpan(value); | 62 this.removeSpan(value); |
63 if (0 <= start && start <= end && end <= this.string_.length) { | 63 if (0 <= start && start <= end && end <= this.string_.length) { |
64 // Zero-length spans are explicitly allowed, because it is possible to | 64 // Zero-length spans are explicitly allowed, because it is possible to |
65 // query for position by annotation as well as the reverse. | 65 // query for position by annotation as well as the reverse. |
66 this.spans_.push({ value: value, start: start, end: end }); | 66 this.spans_.push({value: value, start: start, end: end}); |
67 this.spans_.sort(function(a, b) { | 67 this.spans_.sort(function(a, b) { |
68 var ret = a.start - b.start; | 68 var ret = a.start - b.start; |
69 if (ret == 0) | 69 if (ret == 0) |
70 ret = a.end - b.end; | 70 ret = a.end - b.end; |
71 return ret; | 71 return ret; |
72 }); | 72 }); |
73 } else { | 73 } else { |
74 throw new RangeError('span out of range (start=' + start + | 74 throw new RangeError( |
75 ', end=' + end + ', len=' + this.string_.length + ')'); | 75 'span out of range (start=' + start + ', end=' + end + |
| 76 ', len=' + this.string_.length + ')'); |
76 } | 77 } |
77 }, | 78 }, |
78 | 79 |
79 /** | 80 /** |
80 * Removes a span. | 81 * Removes a span. |
81 * @param {*} value Annotation. | 82 * @param {*} value Annotation. |
82 */ | 83 */ |
83 removeSpan: function(value) { | 84 removeSpan: function(value) { |
84 for (var i = this.spans_.length - 1; i >= 0; i--) { | 85 for (var i = this.spans_.length - 1; i >= 0; i--) { |
85 if (this.spans_[i].value === value) { | 86 if (this.spans_[i].value === value) { |
86 this.spans_.splice(i, 1); | 87 this.spans_.splice(i, 1); |
87 } | 88 } |
88 } | 89 } |
89 }, | 90 }, |
90 | 91 |
91 /** | 92 /** |
92 * Appends another Spannable or string to this one. | 93 * Appends another Spannable or string to this one. |
93 * @param {string|!Spannable} other String or spannable to concatenate. | 94 * @param {string|!Spannable} other String or spannable to concatenate. |
94 */ | 95 */ |
95 append: function(other) { | 96 append: function(other) { |
96 if (other instanceof Spannable) { | 97 if (other instanceof Spannable) { |
97 var otherSpannable = /** @type {!Spannable} */ (other); | 98 var otherSpannable = /** @type {!Spannable} */ (other); |
98 var originalLength = this.length; | 99 var originalLength = this.length; |
99 this.string_ += otherSpannable.string_; | 100 this.string_ += otherSpannable.string_; |
100 other.spans_.forEach(function(span) { | 101 other.spans_.forEach(function(span) { |
101 this.setSpan( | 102 this.setSpan( |
102 span.value, | 103 span.value, span.start + originalLength, span.end + originalLength); |
103 span.start + originalLength, | |
104 span.end + originalLength); | |
105 }.bind(this)); | 104 }.bind(this)); |
106 } else if (typeof other === 'string') { | 105 } else if (typeof other === 'string') { |
107 this.string_ += /** @type {string} */ (other); | 106 this.string_ += /** @type {string} */ (other); |
108 } | 107 } |
109 }, | 108 }, |
110 | 109 |
111 /** | 110 /** |
112 * Returns the first value matching a position. | 111 * Returns the first value matching a position. |
113 * @param {number} position Position to query. | 112 * @param {number} position Position to query. |
114 * @return {*} Value annotating that position, or undefined if none is found. | 113 * @return {*} Value annotating that position, or undefined if none is found. |
(...skipping 12 matching lines...) Expand all Loading... |
127 }, | 126 }, |
128 | 127 |
129 /** | 128 /** |
130 * Returns all span values which are an instance of a given constructor. | 129 * Returns all span values which are an instance of a given constructor. |
131 * Spans are returned in the order of their starting index and ending index | 130 * Spans are returned in the order of their starting index and ending index |
132 * for spans with equals tarting indices. | 131 * for spans with equals tarting indices. |
133 * @param {!Function} constructor Constructor. | 132 * @param {!Function} constructor Constructor. |
134 * @return {!Array<Object>} Array of object. | 133 * @return {!Array<Object>} Array of object. |
135 */ | 134 */ |
136 getSpansInstanceOf: function(constructor) { | 135 getSpansInstanceOf: function(constructor) { |
137 return (this.spans_.filter(spanInstanceOf(constructor)) | 136 return (this.spans_.filter(spanInstanceOf(constructor)).map(valueOfSpan)); |
138 .map(valueOfSpan)); | |
139 }, | 137 }, |
140 | 138 |
141 /** | 139 /** |
142 * Returns all spans matching a position. | 140 * Returns all spans matching a position. |
143 * @param {number} position Position to query. | 141 * @param {number} position Position to query. |
144 * @return {!Array} Values annotating that position. | 142 * @return {!Array} Values annotating that position. |
145 */ | 143 */ |
146 getSpans: function(position) { | 144 getSpans: function(position) { |
147 return (this.spans_ | 145 return (this.spans_.filter(spanCoversPosition(position)).map(valueOfSpan)); |
148 .filter(spanCoversPosition(position)) | |
149 .map(valueOfSpan)); | |
150 }, | 146 }, |
151 | 147 |
152 /** | 148 /** |
153 * Returns whether a span is contained in this object. | 149 * Returns whether a span is contained in this object. |
154 * @param {*} value Annotation. | 150 * @param {*} value Annotation. |
155 * @return {boolean} | 151 * @return {boolean} |
156 */ | 152 */ |
157 hasSpan: function(value) { | 153 hasSpan: function(value) { |
158 return this.spans_.some(spanValueIs(value)); | 154 return this.spans_.some(spanValueIs(value)); |
159 }, | 155 }, |
(...skipping 58 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
218 | 214 |
219 if (start < 0 || end > this.string_.length || start > end) { | 215 if (start < 0 || end > this.string_.length || start > end) { |
220 throw new RangeError('substring indices out of range'); | 216 throw new RangeError('substring indices out of range'); |
221 } | 217 } |
222 | 218 |
223 var result = new Spannable(this.string_.substring(start, end)); | 219 var result = new Spannable(this.string_.substring(start, end)); |
224 this.spans_.forEach(function(span) { | 220 this.spans_.forEach(function(span) { |
225 if (span.start <= end && span.end >= start) { | 221 if (span.start <= end && span.end >= start) { |
226 var newStart = Math.max(0, span.start - start); | 222 var newStart = Math.max(0, span.start - start); |
227 var newEnd = Math.min(end - start, span.end - start); | 223 var newEnd = Math.min(end - start, span.end - start); |
228 result.spans_.push({ value: span.value, start: newStart, end: newEnd }); | 224 result.spans_.push({value: span.value, start: newStart, end: newEnd}); |
229 } | 225 } |
230 }); | 226 }); |
231 return result; | 227 return result; |
232 }, | 228 }, |
233 | 229 |
234 /** | 230 /** |
235 * Trims whitespace from the beginning. | 231 * Trims whitespace from the beginning. |
236 * @return {!Spannable} String with whitespace removed. | 232 * @return {!Spannable} String with whitespace removed. |
237 */ | 233 */ |
238 trimLeft: function() { | 234 trimLeft: function() { |
(...skipping 32 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
271 // As an arbitrary decision, we treat this as trimming the whitespace off | 267 // As an arbitrary decision, we treat this as trimming the whitespace off |
272 // the end, rather than the beginning, of the string. | 268 // the end, rather than the beginning, of the string. |
273 // This choice affects which spans are kept. | 269 // This choice affects which spans are kept. |
274 if (/^\s*$/.test(this.string_)) { | 270 if (/^\s*$/.test(this.string_)) { |
275 return this.substring(0, 0); | 271 return this.substring(0, 0); |
276 } | 272 } |
277 | 273 |
278 // Otherwise, we have at least one non-whitespace character to use as an | 274 // Otherwise, we have at least one non-whitespace character to use as an |
279 // anchor when trimming. | 275 // anchor when trimming. |
280 var trimmedStart = trimStart ? this.string_.match(/^\s*/)[0].length : 0; | 276 var trimmedStart = trimStart ? this.string_.match(/^\s*/)[0].length : 0; |
281 var trimmedEnd = trimEnd ? | 277 var trimmedEnd = |
282 this.string_.match(/\s*$/).index : this.string_.length; | 278 trimEnd ? this.string_.match(/\s*$/).index : this.string_.length; |
283 return this.substring(trimmedStart, trimmedEnd); | 279 return this.substring(trimmedStart, trimmedEnd); |
284 }, | 280 }, |
285 | 281 |
286 /** | 282 /** |
287 * Returns this spannable to a json serializable form, including the text and | 283 * Returns this spannable to a json serializable form, including the text and |
288 * span objects whose types have been registered with registerSerializableSpan | 284 * span objects whose types have been registered with registerSerializableSpan |
289 * or registerStatelessSerializableSpan. | 285 * or registerStatelessSerializableSpan. |
290 * @return {!SerializedSpannable} the json serializable form. | 286 * @return {!SerializedSpannable} the json serializable form. |
291 */ | 287 */ |
292 toJson: function() { | 288 toJson: function() { |
293 var result = {}; | 289 var result = {}; |
294 result.string = this.string_; | 290 result.string = this.string_; |
295 result.spans = []; | 291 result.spans = []; |
296 this.spans_.forEach(function(span) { | 292 this.spans_.forEach(function(span) { |
297 var serializeInfo = serializableSpansByConstructor.get( | 293 var serializeInfo = |
298 span.value.constructor); | 294 serializableSpansByConstructor.get(span.value.constructor); |
299 if (serializeInfo) { | 295 if (serializeInfo) { |
300 var spanObj = {type: serializeInfo.name, | 296 var spanObj = { |
301 start: span.start, | 297 type: serializeInfo.name, |
302 end: span.end}; | 298 start: span.start, |
| 299 end: span.end |
| 300 }; |
303 if (serializeInfo.toJson) { | 301 if (serializeInfo.toJson) { |
304 spanObj.value = serializeInfo.toJson.apply(span.value); | 302 spanObj.value = serializeInfo.toJson.apply(span.value); |
305 } | 303 } |
306 result.spans.push(spanObj); | 304 result.spans.push(spanObj); |
307 } | 305 } |
308 }); | 306 }); |
309 return result; | 307 return result; |
310 } | 308 } |
311 }; | 309 }; |
312 | 310 |
313 /** | 311 /** |
314 * Creates a spannable from a json serializable representation. | 312 * Creates a spannable from a json serializable representation. |
315 * @param {!SerializedSpannable} obj object containing the serializable | 313 * @param {!SerializedSpannable} obj object containing the serializable |
316 * representation. | 314 * representation. |
317 * @return {!Spannable} | 315 * @return {!Spannable} |
318 */ | 316 */ |
319 Spannable.fromJson = function(obj) { | 317 Spannable.fromJson = function(obj) { |
320 if (typeof obj.string !== 'string') { | 318 if (typeof obj.string !== 'string') { |
321 throw new Error( | 319 throw new Error('Invalid spannable json object: string field not a string'); |
322 'Invalid spannable json object: string field not a string'); | |
323 } | 320 } |
324 if (!(obj.spans instanceof Array)) { | 321 if (!(obj.spans instanceof Array)) { |
325 throw new Error('Invalid spannable json object: no spans array'); | 322 throw new Error('Invalid spannable json object: no spans array'); |
326 } | 323 } |
327 var result = new Spannable(obj.string); | 324 var result = new Spannable(obj.string); |
328 result.spans_ = obj.spans.map(function(span) { | 325 result.spans_ = obj.spans.map(function(span) { |
329 if (typeof span.type !== 'string') { | 326 if (typeof span.type !== 'string') { |
330 throw new Error( | 327 throw new Error( |
331 'Invalid span in spannable json object: type not a string'); | 328 'Invalid span in spannable json object: type not a string'); |
332 } | 329 } |
(...skipping 34 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
367 * new objects. | 364 * new objects. |
368 * @param {string} name Name of the type used in the serializable object. | 365 * @param {string} name Name of the type used in the serializable object. |
369 */ | 366 */ |
370 Spannable.registerStatelessSerializableSpan = function(constructor, name) { | 367 Spannable.registerStatelessSerializableSpan = function(constructor, name) { |
371 var obj = {name: name, toJson: undefined}; | 368 var obj = {name: name, toJson: undefined}; |
372 /** | 369 /** |
373 * @param {!Object} obj | 370 * @param {!Object} obj |
374 * @return {!Object} | 371 * @return {!Object} |
375 */ | 372 */ |
376 obj.fromJson = function(obj) { | 373 obj.fromJson = function(obj) { |
377 return new constructor(); | 374 return new constructor(); |
378 }; | 375 }; |
379 serializableSpansByName.set(name, obj); | 376 serializableSpansByName.set(name, obj); |
380 serializableSpansByConstructor.set(constructor, obj); | 377 serializableSpansByConstructor.set(constructor, obj); |
381 }; | 378 }; |
382 | 379 |
383 /** | 380 /** |
384 * An annotation with its start and end points. | 381 * An annotation with its start and end points. |
385 * @typedef {{value: *, start: number, end: number}} | 382 * @typedef {{value: *, start: number, end: number}} |
386 */ | 383 */ |
387 var SpanStruct; | 384 var SpanStruct; |
(...skipping 65 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
453 | 450 |
454 /** | 451 /** |
455 * @param {!SpanStruct|undefined} span | 452 * @param {!SpanStruct|undefined} span |
456 * @return {*} | 453 * @return {*} |
457 */ | 454 */ |
458 function valueOfSpan(span) { | 455 function valueOfSpan(span) { |
459 return span ? span.value : undefined; | 456 return span ? span.value : undefined; |
460 } | 457 } |
461 | 458 |
462 }); | 459 }); |
OLD | NEW |