// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. part of dart._interceptors; /** * The interceptor class for [String]. The compiler recognizes this * class as an interceptor, and changes references to [:this:] to * actually use the receiver of the method, which is generated as an extra * argument added to each member. */ @JsPeerInterface(name: 'String') class JSString extends Interceptor implements String, JSIndexable { const JSString(); @notNull int codeUnitAt(@nullCheck int index) { // Suppress 2nd null check on index and null check on length // (JS String.length cannot be null). final len = this.length; if (index < 0 || index >= len) { throw new RangeError.index(index, this, 'index', null, len); } return JS('int', r'#.charCodeAt(#)', this, index); } @notNull Iterable allMatches(@nullCheck String string, [@nullCheck int start = 0]) { final len = string.length; if (0 > start || start > len) { throw new RangeError.range(start, 0, len); } return allMatchesInStringUnchecked(this, string, start); } Match matchAsPrefix(@nullCheck String string, [@nullCheck int start = 0]) { int stringLength = JS('!', '#.length', string); if (start < 0 || start > stringLength) { throw new RangeError.range(start, 0, stringLength); } int thisLength = JS('!', '#.length', this); if (start + thisLength > stringLength) return null; for (int i = 0; i < thisLength; i++) { if (string.codeUnitAt(start + i) != this.codeUnitAt(i)) { return null; } } return new StringMatch(start, string, this); } @notNull String operator +(@nullCheck String other) { return JS('String', r'# + #', this, other); } @notNull bool endsWith(@nullCheck String other) { var otherLength = other.length; var thisLength = this.length; if (otherLength > thisLength) return false; return other == substring(thisLength - otherLength); } @notNull String replaceAll(Pattern from, @nullCheck String to) { return stringReplaceAllUnchecked(this, from, to); } @notNull String replaceAllMapped(Pattern from, String convert(Match match)) { return this.splitMapJoin(from, onMatch: convert); } @notNull String splitMapJoin(Pattern from, {String onMatch(Match match), String onNonMatch(String nonMatch)}) { return stringReplaceAllFuncUnchecked(this, from, onMatch, onNonMatch); } @notNull String replaceFirst(Pattern from, @nullCheck String to, [@nullCheck int startIndex = 0]) { RangeError.checkValueInInterval(startIndex, 0, this.length, "startIndex"); return stringReplaceFirstUnchecked(this, from, to, startIndex); } @notNull String replaceFirstMapped( Pattern from, @nullCheck String replace(Match match), [@nullCheck int startIndex = 0]) { RangeError.checkValueInInterval(startIndex, 0, this.length, "startIndex"); return stringReplaceFirstMappedUnchecked(this, from, replace, startIndex); } @notNull List split(@nullCheck Pattern pattern) { if (pattern is String) { return new JSArray.of(JS('', r'#.split(#)', this, pattern)); } else if (pattern is JSSyntaxRegExp && regExpCaptureCount(pattern) == 0) { var re = regExpGetNative(pattern); return new JSArray.of(JS('', r'#.split(#)', this, re)); } else { return _defaultSplit(pattern); } } @notNull String replaceRange( @nullCheck int start, int end, @nullCheck String replacement) { end = RangeError.checkValidRange(start, end, this.length); return stringReplaceRangeUnchecked(this, start, end, replacement); } @notNull List _defaultSplit(Pattern pattern) { List result = []; // End of most recent match. That is, start of next part to add to result. int start = 0; // Length of most recent match. // Set >0, so no match on the empty string causes the result to be [""]. int length = 1; for (var match in pattern.allMatches(this)) { @notNull int matchStart = match.start; @notNull int matchEnd = match.end; length = matchEnd - matchStart; if (length == 0 && start == matchStart) { // An empty match right after another match is ignored. // This includes an empty match at the start of the string. continue; } int end = matchStart; result.add(this.substring(start, end)); start = matchEnd; } if (start < this.length || length > 0) { // An empty match at the end of the string does not cause a "" at the end. // A non-empty match ending at the end of the string does add a "". result.add(this.substring(start)); } return result; } @notNull bool startsWith(Pattern pattern, [@nullCheck int index = 0]) { // Suppress null check on length and all but the first // reference to index. int length = JS('int', '#.length', this); if (index < 0 || JS('int', '#', index) > length) { throw new RangeError.range(index, 0, this.length); } if (pattern is String) { String other = pattern; int otherLength = JS('int', '#.length', other); int endIndex = index + otherLength; if (endIndex > length) return false; return other == JS('String', r'#.substring(#, #)', this, index, endIndex); } return pattern.matchAsPrefix(this, index) != null; } @notNull String substring(@nullCheck int startIndex, [int _endIndex]) { var length = this.length; final endIndex = _endIndex ?? length; if (startIndex < 0) throw new RangeError.value(startIndex); if (startIndex > endIndex) throw new RangeError.value(startIndex); if (endIndex > length) throw new RangeError.value(endIndex); return JS('String', r'#.substring(#, #)', this, startIndex, endIndex); } @notNull String toLowerCase() { return JS('String', r'#.toLowerCase()', this); } @notNull String toUpperCase() { return JS('String', r'#.toUpperCase()', this); } // Characters with Whitespace property (Unicode 6.3). // 0009..000D ; White_Space # Cc .. // 0020 ; White_Space # Zs SPACE // 0085 ; White_Space # Cc // 00A0 ; White_Space # Zs NO-BREAK SPACE // 1680 ; White_Space # Zs OGHAM SPACE MARK // 2000..200A ; White_Space # Zs EN QUAD..HAIR SPACE // 2028 ; White_Space # Zl LINE SEPARATOR // 2029 ; White_Space # Zp PARAGRAPH SEPARATOR // 202F ; White_Space # Zs NARROW NO-BREAK SPACE // 205F ; White_Space # Zs MEDIUM MATHEMATICAL SPACE // 3000 ; White_Space # Zs IDEOGRAPHIC SPACE // // BOM: 0xFEFF @notNull static bool _isWhitespace(@notNull int codeUnit) { // Most codeUnits should be less than 256. Special case with a smaller // switch. if (codeUnit < 256) { switch (codeUnit) { case 0x09: case 0x0A: case 0x0B: case 0x0C: case 0x0D: case 0x20: case 0x85: case 0xA0: return true; default: return false; } } switch (codeUnit) { case 0x1680: case 0x2000: case 0x2001: case 0x2002: case 0x2003: case 0x2004: case 0x2005: case 0x2006: case 0x2007: case 0x2008: case 0x2009: case 0x200A: case 0x2028: case 0x2029: case 0x202F: case 0x205F: case 0x3000: case 0xFEFF: return true; default: return false; } } /// Finds the index of the first non-whitespace character, or the /// end of the string. Start looking at position [index]. @notNull static int _skipLeadingWhitespace(String string, @nullCheck int index) { const int SPACE = 0x20; const int CARRIAGE_RETURN = 0x0D; var stringLength = string.length; while (index < stringLength) { int codeUnit = string.codeUnitAt(index); if (codeUnit != SPACE && codeUnit != CARRIAGE_RETURN && !_isWhitespace(codeUnit)) { break; } index++; } return index; } /// Finds the index after the last non-whitespace character, or 0. /// Start looking at position [index - 1]. @notNull static int _skipTrailingWhitespace(String string, @nullCheck int index) { const int SPACE = 0x20; const int CARRIAGE_RETURN = 0x0D; while (index > 0) { int codeUnit = string.codeUnitAt(index - 1); if (codeUnit != SPACE && codeUnit != CARRIAGE_RETURN && !_isWhitespace(codeUnit)) { break; } index--; } return index; } // Dart2js can't use JavaScript trim directly, // because JavaScript does not trim // the NEXT LINE (NEL) character (0x85). @notNull String trim() { const int NEL = 0x85; // Start by doing JS trim. Then check if it leaves a NEL at // either end of the string. String result = JS('!', '#.trim()', this); final length = result.length; if (length == 0) return result; int firstCode = result.codeUnitAt(0); int startIndex = 0; if (firstCode == NEL) { startIndex = _skipLeadingWhitespace(result, 1); if (startIndex == length) return ""; } int endIndex = length; // We know that there is at least one character that is non-whitespace. // Therefore we don't need to verify that endIndex > startIndex. int lastCode = result.codeUnitAt(endIndex - 1); if (lastCode == NEL) { endIndex = _skipTrailingWhitespace(result, endIndex - 1); } if (startIndex == 0 && endIndex == length) return result; return JS('String', r'#.substring(#, #)', result, startIndex, endIndex); } // Dart2js can't use JavaScript trimLeft directly, // because it is not in ES5, so not every browser implements it, // and because those that do will not trim the NEXT LINE character (0x85). @notNull String trimLeft() { const int NEL = 0x85; // Start by doing JS trim. Then check if it leaves a NEL at // the beginning of the string. String result; int startIndex = 0; if (JS('bool', 'typeof #.trimLeft != "undefined"', this)) { result = JS('String', '#.trimLeft()', this); if (result.length == 0) return result; int firstCode = result.codeUnitAt(0); if (firstCode == NEL) { startIndex = _skipLeadingWhitespace(result, 1); } } else { result = this; startIndex = _skipLeadingWhitespace(this, 0); } if (startIndex == 0) return result; if (startIndex == result.length) return ""; return JS('String', r'#.substring(#)', result, startIndex); } // Dart2js can't use JavaScript trimRight directly, // because it is not in ES5 and because JavaScript does not trim // the NEXT LINE character (0x85). @notNull String trimRight() { const int NEL = 0x85; // Start by doing JS trim. Then check if it leaves a NEL or BOM at // the end of the string. String result; @notNull int endIndex = 0; // trimRight is implemented by Firefox and Chrome/Blink, // so use it if it is there. if (JS('bool', 'typeof #.trimRight != "undefined"', this)) { result = JS('String', '#.trimRight()', this); endIndex = result.length; if (endIndex == 0) return result; int lastCode = result.codeUnitAt(endIndex - 1); if (lastCode == NEL) { endIndex = _skipTrailingWhitespace(result, endIndex - 1); } } else { result = this; endIndex = _skipTrailingWhitespace(this, this.length); } if (endIndex == result.length) return result; if (endIndex == 0) return ""; return JS('String', r'#.substring(#, #)', result, 0, endIndex); } @notNull String operator *(@nullCheck int times) { if (0 >= times) return ''; if (times == 1 || this.length == 0) return this; if (times != JS('int', '# >>> 0', times)) { // times >= 2^32. We can't create a string that big. throw const OutOfMemoryError(); } var result = ''; String s = this; while (true) { if (times & 1 == 1) result = s + result; times = JS('int', '# >>> 1', times); if (times == 0) break; s += s; } return result; } @notNull String padLeft(@nullCheck int width, [String padding = ' ']) { int delta = width - this.length; if (delta <= 0) return this; return padding * delta + this; } @notNull String padRight(@nullCheck int width, [String padding = ' ']) { int delta = width - this.length; if (delta <= 0) return this; return this + padding * delta; } @notNull List get codeUnits => new CodeUnits(this); @notNull Runes get runes => new Runes(this); @notNull int indexOf(@nullCheck Pattern pattern, [@nullCheck int start = 0]) { if (start < 0 || start > this.length) { throw new RangeError.range(start, 0, this.length); } if (pattern is String) { return stringIndexOfStringUnchecked(this, pattern, start); } if (pattern is JSSyntaxRegExp) { JSSyntaxRegExp re = pattern; Match match = firstMatchAfter(re, this, start); return (match == null) ? -1 : match.start; } var length = this.length; for (int i = start; i <= length; i++) { if (pattern.matchAsPrefix(this, i) != null) return i; } return -1; } @notNull int lastIndexOf(@nullCheck Pattern pattern, [int _start]) { var length = this.length; var start = _start ?? length; if (start < 0 || start > length) { throw new RangeError.range(start, 0, length); } if (pattern is String) { String other = pattern; if (start + other.length > length) { start = length - other.length; } return stringLastIndexOfUnchecked(this, other, start); } for (int i = start; i >= 0; i--) { if (pattern.matchAsPrefix(this, i) != null) return i; } return -1; } @notNull bool contains(@nullCheck Pattern other, [@nullCheck int startIndex = 0]) { if (startIndex < 0 || startIndex > this.length) { throw new RangeError.range(startIndex, 0, this.length); } return stringContainsUnchecked(this, other, startIndex); } @notNull bool get isEmpty => JS('!', '#.length', this) == 0; @notNull bool get isNotEmpty => !isEmpty; @notNull int compareTo(@nullCheck String other) { return this == other ? 0 : JS('bool', r'# < #', this, other) ? -1 : 1; } // Note: if you change this, also change the function [S]. @notNull String toString() => this; /** * This is the [Jenkins hash function][1] but using masking to keep * values in SMI range. * * [1]: http://en.wikipedia.org/wiki/Jenkins_hash_function */ @notNull int get hashCode { // TODO(ahe): This method shouldn't have to use JS. Update when our // optimizations are smarter. int hash = 0; int length = JS('!', '#.length', this); for (int i = 0; i < length; i++) { hash = 0x1fffffff & (hash + JS('!', r'#.charCodeAt(#)', this, i)); hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); hash = JS('int', '# ^ (# >> 6)', hash, hash); } hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); hash = JS('int', '# ^ (# >> 11)', hash, hash); return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); } @notNull Type get runtimeType => String; @notNull final int length; @notNull String operator [](@nullCheck int index) { if (index >= JS('int', '#.length', this) || index < 0) { throw diagnoseIndexError(this, index); } return JS('String', '#[#]', this, index); } }