| OLD | NEW |
| (Empty) |
| 1 // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file | |
| 2 // for details. All rights reserved. Use of this source code is governed by a | |
| 3 // BSD-style license that can be found in the LICENSE file. | |
| 4 | |
| 5 part of _interceptors; | |
| 6 | |
| 7 /** | |
| 8 * The interceptor class for [String]. The compiler recognizes this | |
| 9 * class as an interceptor, and changes references to [:this:] to | |
| 10 * actually use the receiver of the method, which is generated as an extra | |
| 11 * argument added to each member. | |
| 12 */ | |
| 13 class JSString extends Interceptor implements String, JSIndexable { | |
| 14 const JSString(); | |
| 15 | |
| 16 int codeUnitAt(int index) { | |
| 17 if (index is !int) throw diagnoseIndexError(this, index); | |
| 18 if (index < 0) throw diagnoseIndexError(this, index); | |
| 19 if (index >= length) throw diagnoseIndexError(this, index); | |
| 20 return JS('JSUInt31', r'#.charCodeAt(#)', this, index); | |
| 21 } | |
| 22 | |
| 23 Iterable<Match> allMatches(String string, [int start = 0]) { | |
| 24 checkString(string); | |
| 25 checkInt(start); | |
| 26 if (0 > start || start > string.length) { | |
| 27 throw new RangeError.range(start, 0, string.length); | |
| 28 } | |
| 29 return allMatchesInStringUnchecked(this, string, start); | |
| 30 } | |
| 31 | |
| 32 Match matchAsPrefix(String string, [int start = 0]) { | |
| 33 if (start < 0 || start > string.length) { | |
| 34 throw new RangeError.range(start, 0, string.length); | |
| 35 } | |
| 36 if (start + this.length > string.length) return null; | |
| 37 // TODO(lrn): See if this can be optimized. | |
| 38 for (int i = 0; i < this.length; i++) { | |
| 39 if (string.codeUnitAt(start + i) != this.codeUnitAt(i)) { | |
| 40 return null; | |
| 41 } | |
| 42 } | |
| 43 return new StringMatch(start, string, this); | |
| 44 } | |
| 45 | |
| 46 String operator +(String other) { | |
| 47 if (other is !String) throw new ArgumentError.value(other); | |
| 48 return JS('String', r'# + #', this, other); | |
| 49 } | |
| 50 | |
| 51 bool endsWith(String other) { | |
| 52 checkString(other); | |
| 53 int otherLength = other.length; | |
| 54 if (otherLength > length) return false; | |
| 55 return other == substring(length - otherLength); | |
| 56 } | |
| 57 | |
| 58 String replaceAll(Pattern from, String to) { | |
| 59 checkString(to); | |
| 60 return stringReplaceAllUnchecked(this, from, to); | |
| 61 } | |
| 62 | |
| 63 String replaceAllMapped(Pattern from, String convert(Match match)) { | |
| 64 return this.splitMapJoin(from, onMatch: convert); | |
| 65 } | |
| 66 | |
| 67 String splitMapJoin(Pattern from, | |
| 68 {String onMatch(Match match), | |
| 69 String onNonMatch(String nonMatch)}) { | |
| 70 return stringReplaceAllFuncUnchecked(this, from, onMatch, onNonMatch); | |
| 71 } | |
| 72 | |
| 73 String replaceFirst(Pattern from, String to, [int startIndex = 0]) { | |
| 74 checkString(to); | |
| 75 checkInt(startIndex); | |
| 76 RangeError.checkValueInInterval(startIndex, 0, this.length, "startIndex"); | |
| 77 return stringReplaceFirstUnchecked(this, from, to, startIndex); | |
| 78 } | |
| 79 | |
| 80 String replaceFirstMapped(Pattern from, String replace(Match match), | |
| 81 [int startIndex = 0]) { | |
| 82 checkNull(replace); | |
| 83 checkInt(startIndex); | |
| 84 RangeError.checkValueInInterval(startIndex, 0, this.length, "startIndex"); | |
| 85 return stringReplaceFirstMappedUnchecked(this, from, replace, startIndex); | |
| 86 } | |
| 87 | |
| 88 List<String> split(Pattern pattern) { | |
| 89 checkNull(pattern); | |
| 90 if (pattern is String) { | |
| 91 return JS('JSExtendableArray', r'#.split(#)', this, pattern); | |
| 92 } else if (pattern is JSSyntaxRegExp && regExpCaptureCount(pattern) == 0) { | |
| 93 var re = regExpGetNative(pattern); | |
| 94 return JS('JSExtendableArray', r'#.split(#)', this, re); | |
| 95 } else { | |
| 96 return _defaultSplit(pattern); | |
| 97 } | |
| 98 } | |
| 99 | |
| 100 String replaceRange(int start, int end, String replacement) { | |
| 101 checkString(replacement); | |
| 102 checkInt(start); | |
| 103 end = RangeError.checkValidRange(start, end, this.length); | |
| 104 checkInt(end); | |
| 105 return stringReplaceRangeUnchecked(this, start, end, replacement); | |
| 106 } | |
| 107 | |
| 108 List<String> _defaultSplit(Pattern pattern) { | |
| 109 List<String> result = <String>[]; | |
| 110 // End of most recent match. That is, start of next part to add to result. | |
| 111 int start = 0; | |
| 112 // Length of most recent match. | |
| 113 // Set >0, so no match on the empty string causes the result to be [""]. | |
| 114 int length = 1; | |
| 115 for (var match in pattern.allMatches(this)) { | |
| 116 int matchStart = match.start; | |
| 117 int matchEnd = match.end; | |
| 118 length = matchEnd - matchStart; | |
| 119 if (length == 0 && start == matchStart) { | |
| 120 // An empty match right after another match is ignored. | |
| 121 // This includes an empty match at the start of the string. | |
| 122 continue; | |
| 123 } | |
| 124 int end = matchStart; | |
| 125 result.add(this.substring(start, end)); | |
| 126 start = matchEnd; | |
| 127 } | |
| 128 if (start < this.length || length > 0) { | |
| 129 // An empty match at the end of the string does not cause a "" at the end. | |
| 130 // A non-empty match ending at the end of the string does add a "". | |
| 131 result.add(this.substring(start)); | |
| 132 } | |
| 133 return result; | |
| 134 } | |
| 135 | |
| 136 bool startsWith(Pattern pattern, [int index = 0]) { | |
| 137 checkInt(index); | |
| 138 if (index < 0 || index > this.length) { | |
| 139 throw new RangeError.range(index, 0, this.length); | |
| 140 } | |
| 141 if (pattern is String) { | |
| 142 String other = pattern; | |
| 143 int otherLength = other.length; | |
| 144 int endIndex = index + otherLength; | |
| 145 if (endIndex > length) return false; | |
| 146 return other == JS('String', r'#.substring(#, #)', this, index, endIndex); | |
| 147 } | |
| 148 return pattern.matchAsPrefix(this, index) != null; | |
| 149 } | |
| 150 | |
| 151 String substring(int startIndex, [int endIndex]) { | |
| 152 checkInt(startIndex); | |
| 153 if (endIndex == null) endIndex = length; | |
| 154 checkInt(endIndex); | |
| 155 if (startIndex < 0 ) throw new RangeError.value(startIndex); | |
| 156 if (startIndex > endIndex) throw new RangeError.value(startIndex); | |
| 157 if (endIndex > length) throw new RangeError.value(endIndex); | |
| 158 return JS('String', r'#.substring(#, #)', this, startIndex, endIndex); | |
| 159 } | |
| 160 | |
| 161 String toLowerCase() { | |
| 162 return JS( | |
| 163 'returns:String;effects:none;depends:none;throws:null(1)', | |
| 164 r'#.toLowerCase()', this); | |
| 165 } | |
| 166 | |
| 167 String toUpperCase() { | |
| 168 return JS( | |
| 169 'returns:String;effects:none;depends:none;throws:null(1)', | |
| 170 r'#.toUpperCase()', this); | |
| 171 } | |
| 172 | |
| 173 // Characters with Whitespace property (Unicode 6.2). | |
| 174 // 0009..000D ; White_Space # Cc <control-0009>..<control-000D> | |
| 175 // 0020 ; White_Space # Zs SPACE | |
| 176 // 0085 ; White_Space # Cc <control-0085> | |
| 177 // 00A0 ; White_Space # Zs NO-BREAK SPACE | |
| 178 // 1680 ; White_Space # Zs OGHAM SPACE MARK | |
| 179 // 180E ; White_Space # Zs MONGOLIAN VOWEL SEPARATOR | |
| 180 // 2000..200A ; White_Space # Zs EN QUAD..HAIR SPACE | |
| 181 // 2028 ; White_Space # Zl LINE SEPARATOR | |
| 182 // 2029 ; White_Space # Zp PARAGRAPH SEPARATOR | |
| 183 // 202F ; White_Space # Zs NARROW NO-BREAK SPACE | |
| 184 // 205F ; White_Space # Zs MEDIUM MATHEMATICAL SPACE | |
| 185 // 3000 ; White_Space # Zs IDEOGRAPHIC SPACE | |
| 186 // | |
| 187 // BOM: 0xFEFF | |
| 188 static bool _isWhitespace(int codeUnit) { | |
| 189 // Most codeUnits should be less than 256. Special case with a smaller | |
| 190 // switch. | |
| 191 if (codeUnit < 256) { | |
| 192 switch (codeUnit) { | |
| 193 case 0x09: | |
| 194 case 0x0A: | |
| 195 case 0x0B: | |
| 196 case 0x0C: | |
| 197 case 0x0D: | |
| 198 case 0x20: | |
| 199 case 0x85: | |
| 200 case 0xA0: | |
| 201 return true; | |
| 202 default: | |
| 203 return false; | |
| 204 } | |
| 205 } | |
| 206 switch (codeUnit) { | |
| 207 case 0x1680: | |
| 208 case 0x180E: | |
| 209 case 0x2000: | |
| 210 case 0x2001: | |
| 211 case 0x2002: | |
| 212 case 0x2003: | |
| 213 case 0x2004: | |
| 214 case 0x2005: | |
| 215 case 0x2006: | |
| 216 case 0x2007: | |
| 217 case 0x2008: | |
| 218 case 0x2009: | |
| 219 case 0x200A: | |
| 220 case 0x2028: | |
| 221 case 0x2029: | |
| 222 case 0x202F: | |
| 223 case 0x205F: | |
| 224 case 0x3000: | |
| 225 case 0xFEFF: | |
| 226 return true; | |
| 227 default: | |
| 228 return false; | |
| 229 } | |
| 230 } | |
| 231 | |
| 232 /// Finds the index of the first non-whitespace character, or the | |
| 233 /// end of the string. Start looking at position [index]. | |
| 234 static int _skipLeadingWhitespace(String string, int index) { | |
| 235 const int SPACE = 0x20; | |
| 236 const int CARRIAGE_RETURN = 0x0D; | |
| 237 while (index < string.length) { | |
| 238 int codeUnit = string.codeUnitAt(index); | |
| 239 if (codeUnit != SPACE && | |
| 240 codeUnit != CARRIAGE_RETURN && | |
| 241 !_isWhitespace(codeUnit)) { | |
| 242 break; | |
| 243 } | |
| 244 index++; | |
| 245 } | |
| 246 return index; | |
| 247 } | |
| 248 | |
| 249 /// Finds the index after the the last non-whitespace character, or 0. | |
| 250 /// Start looking at position [index - 1]. | |
| 251 static int _skipTrailingWhitespace(String string, int index) { | |
| 252 const int SPACE = 0x20; | |
| 253 const int CARRIAGE_RETURN = 0x0D; | |
| 254 while (index > 0) { | |
| 255 int codeUnit = string.codeUnitAt(index - 1); | |
| 256 if (codeUnit != SPACE && | |
| 257 codeUnit != CARRIAGE_RETURN && | |
| 258 !_isWhitespace(codeUnit)) { | |
| 259 break; | |
| 260 } | |
| 261 index--; | |
| 262 } | |
| 263 return index; | |
| 264 } | |
| 265 | |
| 266 // Dart2js can't use JavaScript trim directly, | |
| 267 // because JavaScript does not trim | |
| 268 // the NEXT LINE (NEL) character (0x85). | |
| 269 String trim() { | |
| 270 const int NEL = 0x85; | |
| 271 | |
| 272 // Start by doing JS trim. Then check if it leaves a NEL at | |
| 273 // either end of the string. | |
| 274 String result = JS('String', '#.trim()', this); | |
| 275 if (result.length == 0) return result; | |
| 276 int firstCode = result.codeUnitAt(0); | |
| 277 int startIndex = 0; | |
| 278 if (firstCode == NEL) { | |
| 279 startIndex = _skipLeadingWhitespace(result, 1); | |
| 280 if (startIndex == result.length) return ""; | |
| 281 } | |
| 282 | |
| 283 int endIndex = result.length; | |
| 284 // We know that there is at least one character that is non-whitespace. | |
| 285 // Therefore we don't need to verify that endIndex > startIndex. | |
| 286 int lastCode = result.codeUnitAt(endIndex - 1); | |
| 287 if (lastCode == NEL) { | |
| 288 endIndex = _skipTrailingWhitespace(result, endIndex - 1); | |
| 289 } | |
| 290 if (startIndex == 0 && endIndex == result.length) return result; | |
| 291 return JS('String', r'#.substring(#, #)', result, startIndex, endIndex); | |
| 292 } | |
| 293 | |
| 294 // Dart2js can't use JavaScript trimLeft directly, | |
| 295 // because it is not in ES5, so not every browser implements it, | |
| 296 // and because those that do will not trim the NEXT LINE character (0x85). | |
| 297 String trimLeft() { | |
| 298 const int NEL = 0x85; | |
| 299 | |
| 300 // Start by doing JS trim. Then check if it leaves a NEL at | |
| 301 // the beginning of the string. | |
| 302 String result; | |
| 303 int startIndex = 0; | |
| 304 if (JS('bool', 'typeof #.trimLeft != "undefined"', this)) { | |
| 305 result = JS('String', '#.trimLeft()', this); | |
| 306 if (result.length == 0) return result; | |
| 307 int firstCode = result.codeUnitAt(0); | |
| 308 if (firstCode == NEL) { | |
| 309 startIndex = _skipLeadingWhitespace(result, 1); | |
| 310 } | |
| 311 } else { | |
| 312 result = this; | |
| 313 startIndex = _skipLeadingWhitespace(this, 0); | |
| 314 } | |
| 315 if (startIndex == 0) return result; | |
| 316 if (startIndex == result.length) return ""; | |
| 317 return JS('String', r'#.substring(#)', result, startIndex); | |
| 318 } | |
| 319 | |
| 320 // Dart2js can't use JavaScript trimRight directly, | |
| 321 // because it is not in ES5 and because JavaScript does not trim | |
| 322 // the NEXT LINE character (0x85). | |
| 323 String trimRight() { | |
| 324 const int NEL = 0x85; | |
| 325 | |
| 326 // Start by doing JS trim. Then check if it leaves a NEL or BOM at | |
| 327 // the end of the string. | |
| 328 String result; | |
| 329 int endIndex; | |
| 330 // trimRight is implemented by Firefox and Chrome/Blink, | |
| 331 // so use it if it is there. | |
| 332 if (JS('bool', 'typeof #.trimRight != "undefined"', this)) { | |
| 333 result = JS('String', '#.trimRight()', this); | |
| 334 endIndex = result.length; | |
| 335 if (endIndex == 0) return result; | |
| 336 int lastCode = result.codeUnitAt(endIndex - 1); | |
| 337 if (lastCode == NEL) { | |
| 338 endIndex = _skipTrailingWhitespace(result, endIndex - 1); | |
| 339 } | |
| 340 } else { | |
| 341 result = this; | |
| 342 endIndex = _skipTrailingWhitespace(this, this.length); | |
| 343 } | |
| 344 | |
| 345 if (endIndex == result.length) return result; | |
| 346 if (endIndex == 0) return ""; | |
| 347 return JS('String', r'#.substring(#, #)', result, 0, endIndex); | |
| 348 } | |
| 349 | |
| 350 String operator*(int times) { | |
| 351 if (0 >= times) return ''; // Unnecessary but hoists argument type check. | |
| 352 if (times == 1 || this.length == 0) return this; | |
| 353 if (times != JS('JSUInt32', '# >>> 0', times)) { | |
| 354 // times >= 2^32. We can't create a string that big. | |
| 355 throw const OutOfMemoryError(); | |
| 356 } | |
| 357 var result = ''; | |
| 358 var s = this; | |
| 359 while (true) { | |
| 360 if (times & 1 == 1) result = s + result; | |
| 361 times = JS('JSUInt31', '# >>> 1', times); | |
| 362 if (times == 0) break; | |
| 363 s += s; | |
| 364 } | |
| 365 return result; | |
| 366 } | |
| 367 | |
| 368 String padLeft(int width, [String padding = ' ']) { | |
| 369 int delta = width - this.length; | |
| 370 if (delta <= 0) return this; | |
| 371 return padding * delta + this; | |
| 372 } | |
| 373 | |
| 374 String padRight(int width, [String padding = ' ']) { | |
| 375 int delta = width - this.length; | |
| 376 if (delta <= 0) return this; | |
| 377 return this + padding * delta; | |
| 378 } | |
| 379 | |
| 380 List<int> get codeUnits => new CodeUnits(this); | |
| 381 | |
| 382 Runes get runes => new Runes(this); | |
| 383 | |
| 384 int indexOf(Pattern pattern, [int start = 0]) { | |
| 385 checkNull(pattern); | |
| 386 if (start is! int) throw argumentErrorValue(start); | |
| 387 if (start < 0 || start > this.length) { | |
| 388 throw new RangeError.range(start, 0, this.length); | |
| 389 } | |
| 390 if (pattern is String) { | |
| 391 return stringIndexOfStringUnchecked(this, pattern, start); | |
| 392 } | |
| 393 if (pattern is JSSyntaxRegExp) { | |
| 394 JSSyntaxRegExp re = pattern; | |
| 395 Match match = firstMatchAfter(re, this, start); | |
| 396 return (match == null) ? -1 : match.start; | |
| 397 } | |
| 398 for (int i = start; i <= this.length; i++) { | |
| 399 if (pattern.matchAsPrefix(this, i) != null) return i; | |
| 400 } | |
| 401 return -1; | |
| 402 } | |
| 403 | |
| 404 int lastIndexOf(Pattern pattern, [int start]) { | |
| 405 checkNull(pattern); | |
| 406 if (start == null) { | |
| 407 start = length; | |
| 408 } else if (start is! int) { | |
| 409 throw argumentErrorValue(start); | |
| 410 } else if (start < 0 || start > this.length) { | |
| 411 throw new RangeError.range(start, 0, this.length); | |
| 412 } | |
| 413 if (pattern is String) { | |
| 414 String other = pattern; | |
| 415 if (start + other.length > this.length) { | |
| 416 start = this.length - other.length; | |
| 417 } | |
| 418 return stringLastIndexOfUnchecked(this, other, start); | |
| 419 } | |
| 420 for (int i = start; i >= 0; i--) { | |
| 421 if (pattern.matchAsPrefix(this, i) != null) return i; | |
| 422 } | |
| 423 return -1; | |
| 424 } | |
| 425 | |
| 426 bool contains(Pattern other, [int startIndex = 0]) { | |
| 427 checkNull(other); | |
| 428 if (startIndex < 0 || startIndex > this.length) { | |
| 429 throw new RangeError.range(startIndex, 0, this.length); | |
| 430 } | |
| 431 return stringContainsUnchecked(this, other, startIndex); | |
| 432 } | |
| 433 | |
| 434 bool get isEmpty => length == 0; | |
| 435 | |
| 436 bool get isNotEmpty => !isEmpty; | |
| 437 | |
| 438 int compareTo(String other) { | |
| 439 if (other is !String) throw argumentErrorValue(other); | |
| 440 return this == other ? 0 | |
| 441 : JS('bool', r'# < #', this, other) ? -1 : 1; | |
| 442 } | |
| 443 | |
| 444 // Note: if you change this, also change the function [S]. | |
| 445 String toString() => this; | |
| 446 | |
| 447 /** | |
| 448 * This is the [Jenkins hash function][1] but using masking to keep | |
| 449 * values in SMI range. | |
| 450 * | |
| 451 * [1]: http://en.wikipedia.org/wiki/Jenkins_hash_function | |
| 452 */ | |
| 453 int get hashCode { | |
| 454 // TODO(ahe): This method shouldn't have to use JS. Update when our | |
| 455 // optimizations are smarter. | |
| 456 int hash = 0; | |
| 457 for (int i = 0; i < length; i++) { | |
| 458 hash = 0x1fffffff & (hash + JS('int', r'#.charCodeAt(#)', this, i)); | |
| 459 hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); | |
| 460 hash = JS('int', '# ^ (# >> 6)', hash, hash); | |
| 461 } | |
| 462 hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); | |
| 463 hash = JS('int', '# ^ (# >> 11)', hash, hash); | |
| 464 return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); | |
| 465 } | |
| 466 | |
| 467 Type get runtimeType => String; | |
| 468 | |
| 469 int get length => JS('int', r'#.length', this); | |
| 470 | |
| 471 String operator [](int index) { | |
| 472 if (index is !int) throw diagnoseIndexError(this, index); | |
| 473 if (index >= length || index < 0) throw diagnoseIndexError(this, index); | |
| 474 return JS('String', '#[#]', this, index); | |
| 475 } | |
| 476 } | |
| OLD | NEW |