| 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 |