| // Copyright (c) 2023, 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. |
| |
| import 'dart:_error_utils'; |
| import 'dart:_internal'; |
| import 'dart:_js_helper' as js; |
| import 'dart:_js_types'; |
| import 'dart:_object_helper'; |
| import 'dart:_string_helper'; |
| import 'dart:_string' show jsStringFromCharCodeArray; |
| import 'dart:_wasm'; |
| import 'dart:js_interop'; |
| |
| abstract class StringUncheckedOperationsBase { |
| int _codeUnitAtUnchecked(int index); |
| String _substringUnchecked(int start, int end); |
| } |
| |
| extension StringUncheckedOperations on String { |
| @pragma('wasm:prefer-inline') |
| int codeUnitAtUnchecked(int index) => |
| unsafeCast<StringUncheckedOperationsBase>( |
| this, |
| )._codeUnitAtUnchecked(index); |
| |
| @pragma('wasm:prefer-inline') |
| String substringUnchecked(int start, int end) => |
| unsafeCast<StringUncheckedOperationsBase>( |
| this, |
| )._substringUnchecked(start, end); |
| } |
| |
| final class JSStringImpl implements String, StringUncheckedOperationsBase { |
| final WasmExternRef? _ref; |
| |
| JSStringImpl(this._ref); |
| |
| @pragma("wasm:prefer-inline") |
| static String? box(WasmExternRef? ref) => |
| js.isDartNull(ref) ? null : JSStringImpl(ref); |
| |
| @override |
| @pragma("wasm:prefer-inline") |
| int get length => _jsLength(toExternRef); |
| |
| @override |
| @pragma("wasm:prefer-inline") |
| bool get isEmpty => length == 0; |
| |
| @override |
| @pragma("wasm:prefer-inline") |
| bool get isNotEmpty => !isEmpty; |
| |
| @pragma("wasm:entry-point") |
| static String _interpolate(WasmArray<Object?> values) { |
| final valuesLength = values.length; |
| String result = ''; |
| for (int i = 0; i < valuesLength; i++) { |
| result = result + values[i].toString(); |
| } |
| return result; |
| } |
| |
| @pragma("wasm:entry-point", "call") |
| static String _interpolate1(Object? value) { |
| return value is String ? value : value.toString(); |
| } |
| |
| @pragma("wasm:entry-point", "call") |
| static String _interpolate2(Object? value1, Object? value2) { |
| final String string1 = value1 is String ? value1 : value1.toString(); |
| final String string2 = value2 is String ? value2 : value2.toString(); |
| return JSStringImpl( |
| _jsStringConcatImport( |
| unsafeCast<JSStringImpl>(string1).toExternRef, |
| unsafeCast<JSStringImpl>(string2).toExternRef, |
| ), |
| ); |
| } |
| |
| @pragma("wasm:entry-point", "call") |
| static String _interpolate3(Object? value1, Object? value2, Object? value3) { |
| final String string1 = value1 is String ? value1 : value1.toString(); |
| final String string2 = value2 is String ? value2 : value2.toString(); |
| final String string3 = value3 is String ? value3 : value3.toString(); |
| return JSStringImpl( |
| _jsStringConcatImport( |
| _jsStringConcatImport( |
| unsafeCast<JSStringImpl>(string1).toExternRef, |
| unsafeCast<JSStringImpl>(string2).toExternRef, |
| ), |
| unsafeCast<JSStringImpl>(string3).toExternRef, |
| ), |
| ); |
| } |
| |
| @pragma("wasm:entry-point", "call") |
| static String _interpolate4( |
| Object? value1, |
| Object? value2, |
| Object? value3, |
| Object? value4, |
| ) { |
| final String string1 = value1 is String ? value1 : value1.toString(); |
| final String string2 = value2 is String ? value2 : value2.toString(); |
| final String string3 = value3 is String ? value3 : value3.toString(); |
| final String string4 = value4 is String ? value4 : value4.toString(); |
| return JSStringImpl( |
| _jsStringConcatImport( |
| _jsStringConcatImport( |
| unsafeCast<JSStringImpl>(string1).toExternRef, |
| unsafeCast<JSStringImpl>(string2).toExternRef, |
| ), |
| _jsStringConcatImport( |
| unsafeCast<JSStringImpl>(string3).toExternRef, |
| unsafeCast<JSStringImpl>(string4).toExternRef, |
| ), |
| ), |
| ); |
| } |
| |
| static const _codeUnitsCacheSize = 512; |
| @pragma('wasm:initialize-at-startup') |
| static final _codeUnitsCache = WasmArray<WasmI16>(_codeUnitsCacheSize); |
| static JSStringImpl fromAsciiBytes( |
| WasmArray<WasmI8> source, |
| int start, |
| int end, |
| ) { |
| final length = end - start; |
| if (length <= _codeUnitsCacheSize) { |
| final array = _codeUnitsCache; |
| for (int i = 0; i < length; ++i) { |
| array.write(i, source.readUnsigned(start++)); |
| } |
| return fromCharCodeArray(array, 0, length); |
| } |
| final array = WasmArray<WasmI16>(length); |
| for (int i = 0; i < length; ++i) { |
| array.write(i, source.readUnsigned(start++)); |
| } |
| return fromCharCodeArray(array, 0, length); |
| } |
| |
| static JSStringImpl fromCharCodeArray( |
| WasmArray<WasmI16> source, |
| int start, |
| int end, |
| ) { |
| return JSStringImpl( |
| jsStringFromCharCodeArray(source, start.toWasmI32(), end.toWasmI32()), |
| ); |
| } |
| |
| @pragma("wasm:initialize-at-startup") |
| static final _stringFromCodePointBuffer = WasmArray<WasmI16>(2); |
| |
| static JSStringImpl fromCharCode(int charCode) { |
| final array = _stringFromCodePointBuffer; |
| array.write(0, charCode); |
| return JSStringImpl.fromCharCodeArray(array, 0, 1); |
| } |
| |
| static JSStringImpl fromCodePoint(int codePoint) { |
| final array = _stringFromCodePointBuffer; |
| if (codePoint <= 0xffff) { |
| array.write(0, codePoint); |
| return JSStringImpl.fromCharCodeArray(array, 0, 1); |
| } |
| final low = 0xDC00 | (codePoint & 0x3ff); |
| final high = 0xD7C0 + (codePoint >> 10); |
| array.write(0, high); |
| array.write(1, low); |
| return JSStringImpl.fromCharCodeArray(array, 0, 2); |
| } |
| |
| @override |
| @pragma("wasm:prefer-inline") |
| int codeUnitAt(int index) { |
| final length = this.length; |
| IndexErrorUtils.checkIndex(index, length); |
| return _codeUnitAtUnchecked(index); |
| } |
| |
| @override |
| @pragma("wasm:prefer-inline") |
| int _codeUnitAtUnchecked(int index) { |
| return jsCharCodeAt(toExternRef, index); |
| } |
| |
| @override |
| Iterable<Match> allMatches(String string, [int start = 0]) { |
| final stringLength = string.length; |
| RangeErrorUtils.checkValueBetweenZeroAndPositiveMax(start, stringLength); |
| return StringAllMatchesIterable(string, this, start); |
| } |
| |
| @override |
| Match? matchAsPrefix(String string, [int start = 0]) { |
| final stringLength = string.length; |
| RangeErrorUtils.checkValueBetweenZeroAndPositiveMax(start, stringLength); |
| final length = this.length; |
| if (start + length > stringLength) return null; |
| // TODO(lrn): See if this can be optimized. |
| for (int i = 0; i < length; i++) { |
| if (string.codeUnitAt(start + i) != codeUnitAt(i)) { |
| return null; |
| } |
| } |
| return StringMatch(start, string, this); |
| } |
| |
| @override |
| @pragma('dyn-module:callable') |
| String operator +(String other) { |
| return JSStringImpl( |
| _jsStringConcatImport( |
| toExternRef, |
| unsafeCast<JSStringImpl>(other).toExternRef, |
| ), |
| ); |
| } |
| |
| @override |
| bool endsWith(String other) { |
| final otherLength = other.length; |
| final length = this.length; |
| if (otherLength > length) return false; |
| return other == _substringUnchecked(length - otherLength, length); |
| } |
| |
| @override |
| String replaceAll(Pattern from, String to) { |
| if (from is String) { |
| if (from.isEmpty) { |
| if (isEmpty) return to; |
| StringBuffer result = StringBuffer(); |
| result.write(to); |
| final length = this.length; |
| for (int i = 0; i < length; i++) { |
| result.write(this[i]); |
| result.write(to); |
| } |
| return result.toString(); |
| } |
| return _jsStringReplaceAll( |
| toExternRef, |
| unsafeCast<JSStringImpl>(from).toExternRef, |
| to.toExternRef, |
| ); |
| } |
| |
| if (from is js.JSSyntaxRegExp) { |
| return _jsStringReplace( |
| toExternRef, |
| (js.regExpGetGlobalNative(from) as js.JSValue).toExternRef, |
| to.toExternRef, |
| ); |
| } else { |
| int startIndex = 0; |
| StringBuffer result = StringBuffer(); |
| for (Match match in from.allMatches(this)) { |
| result.write(substring(startIndex, match.start)); |
| result.write(to); |
| startIndex = match.end; |
| } |
| result.write(substring(startIndex)); |
| return result.toString(); |
| } |
| } |
| |
| @override |
| String replaceAllMapped(Pattern from, String Function(Match) convert) { |
| return splitMapJoin(from, onMatch: convert); |
| } |
| |
| @override |
| String splitMapJoin( |
| Pattern from, { |
| String Function(Match)? onMatch, |
| String Function(String)? onNonMatch, |
| }) { |
| if (onMatch == null) onMatch = _matchString; |
| if (onNonMatch == null) onNonMatch = _stringIdentity; |
| if (from is String) { |
| final patternLength = from.length; |
| if (patternLength == 0) { |
| // Pattern is the empty string. |
| StringBuffer buffer = StringBuffer(); |
| int i = 0; |
| buffer.write(onNonMatch("")); |
| final length = this.length; |
| while (i < length) { |
| buffer.write(onMatch(StringMatch(i, this, ""))); |
| // Special case to avoid splitting a surrogate pair. |
| int code = codeUnitAt(i); |
| if ((code & ~0x3FF) == 0xD800 && length > i + 1) { |
| // Leading surrogate; |
| code = codeUnitAt(i + 1); |
| if ((code & ~0x3FF) == 0xDC00) { |
| // Matching trailing surrogate. |
| buffer.write(onNonMatch(substring(i, i + 2))); |
| i += 2; |
| continue; |
| } |
| } |
| buffer.write(onNonMatch(this[i])); |
| i++; |
| } |
| buffer.write(onMatch(StringMatch(i, this, ""))); |
| buffer.write(onNonMatch("")); |
| return buffer.toString(); |
| } |
| StringBuffer buffer = StringBuffer(); |
| int startIndex = 0; |
| final length = this.length; |
| while (startIndex < length) { |
| int position = indexOf(from, startIndex); |
| if (position == -1) { |
| break; |
| } |
| buffer.write(onNonMatch(substring(startIndex, position))); |
| buffer.write(onMatch(StringMatch(position, this, from))); |
| startIndex = position + patternLength; |
| } |
| buffer.write(onNonMatch(substring(startIndex))); |
| return buffer.toString(); |
| } |
| StringBuffer buffer = StringBuffer(); |
| int startIndex = 0; |
| for (Match match in from.allMatches(this)) { |
| buffer.write(onNonMatch(substring(startIndex, match.start))); |
| buffer.write(onMatch(match)); |
| startIndex = match.end; |
| } |
| buffer.write(onNonMatch(substring(startIndex))); |
| return buffer.toString(); |
| } |
| |
| String _replaceRange(int start, int end, String replacement) { |
| String prefix = substring(0, start); |
| String suffix = substring(end); |
| return "$prefix$replacement$suffix"; |
| } |
| |
| String _replaceFirstRE( |
| js.JSSyntaxRegExp regexp, |
| String replacement, |
| int startIndex, |
| ) { |
| final match = js.regExpExecGlobal(regexp, this.toJS, startIndex); |
| if (match == null) return this; |
| final start = match.start; |
| final end = match.end; |
| return _replaceRange(start, end, replacement); |
| } |
| |
| @override |
| String replaceFirst(Pattern from, String to, [int startIndex = 0]) { |
| RangeErrorUtils.checkValueBetweenZeroAndPositiveMax(startIndex, length); |
| if (from is String) { |
| int index = indexOf(from, startIndex); |
| if (index < 0) return this; |
| int end = index + from.length; |
| return _replaceRange(index, end, to); |
| } |
| if (from is js.JSSyntaxRegExp) { |
| return startIndex == 0 |
| ? _jsStringReplace( |
| toExternRef, |
| (js.regExpGetNative(from) as js.JSValue).toExternRef, |
| to.toExternRef, |
| ) |
| : _replaceFirstRE(from, to, startIndex); |
| } |
| Iterator<Match> matches = from.allMatches(this, startIndex).iterator; |
| if (!matches.moveNext()) return this; |
| Match match = matches.current; |
| return replaceRange(match.start, match.end, to); |
| } |
| |
| @override |
| String replaceFirstMapped( |
| Pattern from, |
| String replace(Match match), [ |
| int startIndex = 0, |
| ]) { |
| RangeErrorUtils.checkValueBetweenZeroAndPositiveMax(startIndex, length); |
| Iterator<Match> matches = from.allMatches(this, startIndex).iterator; |
| if (!matches.moveNext()) return this; |
| Match match = matches.current; |
| String replacement = "${replace(match)}"; |
| return replaceRange(match.start, match.end, replacement); |
| } |
| |
| @override |
| List<String> split(Pattern pattern) { |
| if (pattern is JSStringImpl) { |
| return _jsStringSplitToDart(toExternRef, pattern.toExternRef); |
| } else if (pattern is String) { |
| return _jsStringSplitToDart(toExternRef, pattern.toJS.toExternRef); |
| } else if (pattern is js.JSSyntaxRegExp && |
| js.regExpCaptureCount(pattern) == 0) { |
| final re = js.regExpGetNative(pattern); |
| return _jsStringSplitToDart(toExternRef, (re as js.JSValue).toExternRef); |
| } else { |
| final result = <String>[]; |
| // 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)) { |
| int matchStart = match.start; |
| 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(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(substring(start)); |
| } |
| return result; |
| } |
| } |
| |
| @override |
| String replaceRange(int start, int? end, String replacement) { |
| end ??= length; |
| RangeErrorUtils.checkValidRange(start, end, length); |
| return _replaceRange(start, end, replacement); |
| } |
| |
| @override |
| bool startsWith(Pattern pattern, [int index = 0]) { |
| RangeErrorUtils.checkValueBetweenZeroAndPositiveMax(index, length); |
| if (pattern is String) { |
| final patternLength = pattern.length; |
| final endIndex = index + patternLength; |
| if (endIndex > length) return false; |
| return pattern == substring(index, endIndex); |
| } |
| return pattern.matchAsPrefix(this, index) != null; |
| } |
| |
| @override |
| String substring(int start, [int? end]) { |
| end ??= length; |
| RangeErrorUtils.checkValidRange(start, end, length); |
| if (start == end) return ""; |
| return _substringUnchecked(start, end); |
| } |
| |
| @override |
| @pragma('wasm:prefer-inline') |
| String _substringUnchecked(int start, int end) => |
| JSStringImpl(_jsSubstring(toExternRef, start, end)); |
| |
| @override |
| String toLowerCase() { |
| final thisRef = toExternRef; |
| final lowerCaseRef = _jsStringToLowerCase(thisRef); |
| return _jsIdentical(thisRef, lowerCaseRef) |
| ? this |
| : JSStringImpl(lowerCaseRef); |
| } |
| |
| @override |
| String toUpperCase() { |
| final thisRef = toExternRef; |
| final upperCaseRef = _jsStringToUpperCase(thisRef); |
| return _jsIdentical(thisRef, upperCaseRef) |
| ? this |
| : JSStringImpl(upperCaseRef); |
| } |
| |
| // Characters with Whitespace property (Unicode 6.3). |
| // 0009..000D ; White_Space # Cc <control-0009>..<control-000D> |
| // 0020 ; White_Space # Zs SPACE |
| // 0085 ; White_Space # Cc <control-0085> |
| // 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 |
| static bool _isWhitespace(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; |
| } |
| } |
| |
| static const int spaceCodeUnit = 0x20; |
| static const int carriageReturnCodeUnit = 0x0D; |
| static const int nelCodeUnit = 0x85; |
| |
| /// Finds the index of the first non-whitespace character, or the |
| /// end of the string. Start looking at position [index]. |
| static int _skipLeadingWhitespace(JSStringImpl string, int index) { |
| final stringLength = string.length; |
| while (index < stringLength) { |
| int codeUnit = string._codeUnitAtUnchecked(index); |
| if (codeUnit != spaceCodeUnit && |
| codeUnit != carriageReturnCodeUnit && |
| !_isWhitespace(codeUnit)) { |
| break; |
| } |
| index++; |
| } |
| return index; |
| } |
| |
| /// Finds the index after the last non-whitespace character, or 0. |
| /// Start looking at position [index - 1]. |
| static int _skipTrailingWhitespace(JSStringImpl string, int index) { |
| while (index > 0) { |
| int codeUnit = string._codeUnitAtUnchecked(index - 1); |
| if (codeUnit != spaceCodeUnit && |
| codeUnit != carriageReturnCodeUnit && |
| !_isWhitespace(codeUnit)) { |
| break; |
| } |
| index--; |
| } |
| return index; |
| } |
| |
| // dart2wasm can't use JavaScript trim directly, because JavaScript does not |
| // trim the NEXT LINE (NEL) character (0x85). |
| @override |
| String trim() { |
| final length = this.length; |
| if (length == 0) return this; |
| |
| // Start by doing JS trim. Then check if it leaves a NEL at either end of |
| // the string. |
| final result = JSStringImpl(_jsStringTrim(toExternRef)); |
| final resultLength = result.length; |
| if (resultLength == 0) return result; |
| |
| // Check NEL on the left. |
| final int firstCode = result._codeUnitAtUnchecked(0); |
| int startIndex = 0; |
| if (firstCode == nelCodeUnit) { |
| startIndex = _skipLeadingWhitespace(result, 1); |
| if (startIndex == resultLength) return ""; |
| } |
| |
| // Check NEL on the right. |
| int endIndex = resultLength; |
| // We know that there is at least one character that is non-whitespace. |
| // Therefore we don't need to verify that endIndex > startIndex. |
| final int lastCode = result.codeUnitAt(endIndex - 1); |
| if (lastCode == nelCodeUnit) { |
| endIndex = _skipTrailingWhitespace(result, endIndex - 1); |
| } |
| |
| if (startIndex == 0 && endIndex == resultLength) { |
| return length == resultLength ? this : result; |
| } |
| |
| return result.substring(startIndex, endIndex); |
| } |
| |
| // dart2wasm can't use JavaScript trimLeft directly because it does not trim |
| // the NEXT LINE (NEL) character (0x85). |
| @override |
| String trimLeft() { |
| final length = this.length; |
| if (length == 0) return this; |
| |
| // Start by doing JS trim. Then check if it leaves a NEL at the beginning |
| // of the string. |
| int startIndex = 0; |
| final result = JSStringImpl(_jsStringTrimLeft(toExternRef)); |
| final resultLength = result.length; |
| if (resultLength == 0) return result; |
| |
| // Check NEL. |
| int firstCode = result._codeUnitAtUnchecked(0); |
| if (firstCode == nelCodeUnit) { |
| startIndex = _skipLeadingWhitespace(result, 1); |
| } |
| |
| if (startIndex == 0) { |
| return resultLength == length ? this : result; |
| } |
| |
| return result.substring(startIndex); |
| } |
| |
| // dart2wasm can't use JavaScript trimRight directly because it does not trim |
| // the NEXT LINE (NEL) character (0x85). |
| @override |
| String trimRight() { |
| final length = this.length; |
| if (length == 0) return this; |
| |
| // Start by doing JS trim. Then check if it leaves a NEL at the end of the |
| // string. |
| final result = JSStringImpl(_jsStringTrimRight(toExternRef)); |
| final resultLength = result.length; |
| if (resultLength == 0) return result; |
| |
| int endIndex = resultLength; |
| int lastCode = result.codeUnitAt(endIndex - 1); |
| if (lastCode == nelCodeUnit) { |
| endIndex = _skipTrailingWhitespace(result, endIndex - 1); |
| } |
| |
| if (endIndex == resultLength) { |
| return resultLength == length ? this : result; |
| } |
| |
| return result.substring(0, endIndex); |
| } |
| |
| @override |
| String operator *(int times) { |
| if (0 >= times) return ''; |
| if (times == 1 || length == 0) return this; |
| if (times & 0x7fffffff != times) { |
| throw Exception( |
| 'The implementation cannot handle very large operands (was: $times).', |
| ); |
| } |
| return JSStringImpl(_jsStringRepeat(toExternRef, times)); |
| } |
| |
| @override |
| String padLeft(int width, [String padding = ' ']) { |
| int delta = width - length; |
| if (delta <= 0) return this; |
| return (padding * delta) + this; |
| } |
| |
| @override |
| String padRight(int width, [String padding = ' ']) { |
| int delta = width - length; |
| if (delta <= 0) return this; |
| return this + (padding * delta); |
| } |
| |
| @override |
| List<int> get codeUnits => CodeUnits(this); |
| |
| @override |
| Runes get runes => Runes(this); |
| |
| @override |
| int indexOf(Pattern pattern, [int start = 0]) { |
| final length = this.length; |
| RangeErrorUtils.checkValueBetweenZeroAndPositiveMax(start, length); |
| if (pattern is JSStringImpl) { |
| return _jsStringIndexOf(toExternRef, pattern.toExternRef, start); |
| } else if (pattern is String) { |
| return _jsStringIndexOf(toExternRef, pattern.toJS.toExternRef, start); |
| } else if (pattern is js.JSSyntaxRegExp) { |
| Match? match = js.firstMatchAfter(pattern, this.toJS, start); |
| return (match == null) ? -1 : match.start; |
| } else { |
| for (int i = start; i <= length; i++) { |
| if (pattern.matchAsPrefix(this, i) != null) return i; |
| } |
| return -1; |
| } |
| } |
| |
| @override |
| int lastIndexOf(Pattern pattern, [int? start]) { |
| final length = this.length; |
| if (start == null) { |
| start = length; |
| } else { |
| RangeErrorUtils.checkValueBetweenZeroAndPositiveMax(start, length); |
| } |
| if (pattern is JSStringImpl) { |
| if (start + pattern.length > length) { |
| start = length - pattern.length; |
| } |
| return _jsStringLastIndexOf(toExternRef, pattern.toExternRef, start); |
| } else if (pattern is String) { |
| if (start + pattern.length > length) { |
| start = length - pattern.length; |
| } |
| return _jsStringLastIndexOf(toExternRef, pattern.toJS.toExternRef, start); |
| } |
| for (int i = start; i >= 0; i--) { |
| if (pattern.matchAsPrefix(this, i) != null) return i; |
| } |
| return -1; |
| } |
| |
| @override |
| bool contains(Pattern other, [int startIndex = 0]) { |
| final length = this.length; |
| RangeErrorUtils.checkValueBetweenZeroAndPositiveMax(startIndex, length); |
| if (other is String) { |
| return indexOf(other, startIndex) >= 0; |
| } else if (other is js.JSSyntaxRegExp) { |
| return other.hasMatch(substring(startIndex)); |
| } else { |
| return other.allMatches(substring(startIndex)).isNotEmpty; |
| } |
| } |
| |
| @override |
| int get hashCode { |
| int hash = getIdentityHashField(this); |
| if (hash != 0) return hash; |
| hash = _computeHashCode(); |
| setIdentityHashField(this, hash); |
| return hash; |
| } |
| |
| /// This must be kept in sync with `StringBase.hashCode` in string_patch.dart. |
| int _computeHashCode() { |
| int hash = 0; |
| final length = this.length; |
| for (int i = 0; i < length; i++) { |
| hash = stringCombineHashes(hash, _codeUnitAtUnchecked(i)); |
| } |
| return stringFinalizeHash(hash); |
| } |
| |
| @override |
| @pragma("wasm:prefer-inline") |
| String operator [](int index) { |
| IndexErrorUtils.checkIndex(index, length); |
| return JSStringImpl.fromCharCode(_codeUnitAtUnchecked(index)); |
| } |
| |
| @override |
| @pragma('wasm:prefer-inline') |
| bool operator ==(Object other) => |
| other is JSStringImpl && _jsEquals(toExternRef, other.toExternRef); |
| |
| @override |
| @pragma('wasm:prefer-inline') |
| int compareTo(String other) => |
| _jsCompare(toExternRef, unsafeCast<JSStringImpl>(other).toExternRef); |
| |
| @override |
| String toString() => this; |
| |
| int firstNonWhitespace() { |
| final length = this.length; |
| int first = 0; |
| for (; first < length; first++) { |
| if (!_isWhitespace(_codeUnitAtUnchecked(first))) { |
| break; |
| } |
| } |
| return first; |
| } |
| |
| int lastNonWhitespace() { |
| int last = length - 1; |
| for (; last >= 0; last--) { |
| if (!_isWhitespace(_codeUnitAtUnchecked(last))) { |
| break; |
| } |
| } |
| return last; |
| } |
| } |
| |
| extension JSStringImplExt on JSStringImpl { |
| @pragma("wasm:prefer-inline") |
| WasmExternRef? get toExternRef => _ref; |
| } |
| |
| String _matchString(Match match) => match[0]!; |
| |
| String _stringIdentity(String string) => string; |
| |
| // NOTE: The [replacement] does not need special escaping it will be used |
| // as-is (due to passing `() => r` to the JS `replaceAll()` call) |
| JSStringImpl _jsStringReplaceAll( |
| WasmExternRef? string, |
| WasmExternRef? pattern, |
| WasmExternRef? replacement, |
| ) => JSStringImpl( |
| js.JS<WasmExternRef?>( |
| '(o, p, r) => o.replaceAll(p, () => r)', |
| string, |
| pattern, |
| replacement, |
| ), |
| ); |
| |
| // NOTE: The [replacement] does not need special escaping it will be used |
| // as-is (due to passing `() => r` to the JS `replaceAll()` call) |
| String _jsStringReplace( |
| WasmExternRef? string, |
| WasmExternRef? pattern, |
| WasmExternRef? replacement, |
| ) => JSStringImpl( |
| js.JS<WasmExternRef?>( |
| '(o, p, r) => o.replace(p, () => r)', |
| string, |
| pattern, |
| replacement, |
| ), |
| ); |
| |
| WasmExternRef? _jsStringToLowerCase(WasmExternRef? string) => |
| // Note: Special cased in V8 to be fast as `F.p.c.b`. |
| // Do not change to `s => s.toLowerCase()`! |
| js.JS<WasmExternRef?>( |
| 'Function.prototype.call.bind(String.prototype.toLowerCase)', |
| string, |
| ); |
| |
| WasmExternRef? _jsStringToUpperCase(WasmExternRef? string) => |
| js.JS<WasmExternRef?>('s => s.toUpperCase()', string); |
| |
| WasmExternRef? _jsStringTrim(WasmExternRef? string) => |
| js.JS<WasmExternRef?>('s => s.trim()', string); |
| |
| WasmExternRef? _jsStringTrimLeft(WasmExternRef? string) => |
| js.JS<WasmExternRef?>('s => s.trimLeft()', string); |
| |
| WasmExternRef? _jsStringTrimRight(WasmExternRef? string) => |
| js.JS<WasmExternRef?>('s => s.trimRight()', string); |
| |
| @pragma("wasm:prefer-inline") |
| WasmExternRef? _jsStringRepeat(WasmExternRef? string, int times) => |
| js.JS<WasmExternRef?>( |
| '(string, times) => string.repeat(times)', |
| string, |
| times.toWasmI32(), |
| ); |
| |
| @pragma("wasm:prefer-inline") |
| int _jsStringIndexOf( |
| WasmExternRef? string, |
| WasmExternRef? pattern, |
| int startIndex, |
| ) => |
| // Note: Special cased in V8 to be fast as `F.p.c.b`. |
| // Do not change to `(s, p, i) => s.indexOf(p, i)`! |
| js |
| .JS<double>( |
| 'Function.prototype.call.bind(String.prototype.indexOf)', |
| string, |
| pattern, |
| startIndex.toWasmI32(), |
| ) |
| .toInt(); |
| |
| @pragma("wasm:prefer-inline") |
| int _jsStringLastIndexOf( |
| WasmExternRef? string, |
| WasmExternRef? pattern, |
| int start, |
| ) => js |
| .JS<double>( |
| '(s, p, i) => s.lastIndexOf(p, i)', |
| string, |
| pattern, |
| start.toDouble(), |
| ) |
| .toInt(); |
| |
| // TODO(joshualitt): Create a subtype of `JSArrayImpl` that can support lazily |
| // converting arguments `toDart` and return that here. |
| @pragma("wasm:prefer-inline") |
| List<String> _jsStringSplitToDart( |
| WasmExternRef? string, |
| WasmExternRef? token, |
| ) => (js.JSValue(_jsStringSplit(string, token)) as JSArray).toDart |
| .map((JSAny? a) => (a as JSString).toDart) |
| .toList(); |
| |
| WasmExternRef? _jsStringSplit(WasmExternRef? string, WasmExternRef? token) => |
| js.JS<WasmExternRef?>( |
| '(string, token) => string.split(token)', |
| string, |
| token, |
| ); |
| |
| bool _jsIdentical(WasmExternRef? ref1, WasmExternRef? ref2) => |
| js.JS<bool>('Object.is', ref1, ref2); |
| |
| @pragma("wasm:prefer-inline") |
| int jsCharCodeAt(WasmExternRef? stringRef, int index) => |
| _jsStringCharCodeAtImport( |
| stringRef, |
| WasmI32.fromInt(index), |
| ).toIntUnsigned(); |
| |
| @pragma("wasm:prefer-inline") |
| WasmExternRef _jsSubstring( |
| WasmExternRef? stringRef, |
| int startIndex, |
| int endIndex, |
| ) => _jsStringSubstringImport( |
| stringRef, |
| WasmI32.fromInt(startIndex), |
| WasmI32.fromInt(endIndex), |
| ); |
| |
| @pragma("wasm:prefer-inline") |
| int _jsLength(WasmExternRef? stringRef) => |
| _jsStringLengthImport(stringRef).toIntUnsigned(); |
| |
| @pragma("wasm:prefer-inline") |
| bool _jsEquals(WasmExternRef? s1, WasmExternRef? s2) => |
| _jsStringEqualsImport(s1, s2).toBool(); |
| |
| @pragma("wasm:prefer-inline") |
| int _jsCompare(WasmExternRef? s1, WasmExternRef? s2) => |
| _jsStringCompareImport(s1, s2).toIntSigned(); |
| |
| @pragma("wasm:import", "wasm:js-string.charCodeAt") |
| external WasmI32 _jsStringCharCodeAtImport(WasmExternRef? s, WasmI32 index); |
| |
| @pragma("wasm:import", "wasm:js-string.compare") |
| external WasmI32 _jsStringCompareImport(WasmExternRef? s1, WasmExternRef? s2); |
| |
| @pragma("wasm:import", "wasm:js-string.concat") |
| external WasmExternRef _jsStringConcatImport( |
| WasmExternRef? s1, |
| WasmExternRef? s2, |
| ); |
| |
| @pragma("wasm:import", "wasm:js-string.equals") |
| external WasmI32 _jsStringEqualsImport(WasmExternRef? s1, WasmExternRef? s2); |
| |
| @pragma("wasm:import", "wasm:js-string.fromCharCode") |
| external WasmExternRef _jsStringFromCharCodeImport(WasmI32 c); |
| |
| @pragma("wasm:import", "wasm:js-string.fromCodePoint") |
| external WasmExternRef _jsStringFromCodePointImport(WasmI32 c); |
| |
| @pragma("wasm:import", "wasm:js-string.length") |
| external WasmI32 _jsStringLengthImport(WasmExternRef? s); |
| |
| @pragma("wasm:import", "wasm:js-string.substring") |
| external WasmExternRef _jsStringSubstringImport( |
| WasmExternRef? s, |
| WasmI32 startIndex, |
| WasmI32 endIndex, |
| ); |
| |
| @pragma("wasm:import", "wasm:js-string.fromCharCodeArray") |
| external WasmExternRef jsStringFromCharCodeArray( |
| WasmArray<WasmI16>? array, |
| WasmI32 start, |
| WasmI32 end, |
| ); |
| |
| @pragma("wasm:import", "wasm:js-string.intoCharCodeArray") |
| external WasmI32 jsStringIntoCharCodeArray( |
| WasmExternRef? s, |
| WasmArray<WasmI16>? array, |
| WasmI32 start, |
| ); |