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