blob: 9305f55976f880a993919b805a1904a0a2a3cfee [file] [log] [blame]
// 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 _js_helper;
int stringIndexOfStringUnchecked(
String receiver, String other, int startIndex) {
return JS('int', '#.indexOf(#, #)', receiver, other, startIndex);
}
String substring1Unchecked(String receiver, int startIndex) {
return JS('String', '#.substring(#)', receiver, startIndex);
}
String substring2Unchecked(String receiver, int startIndex, int endIndex) {
return JS('String', '#.substring(#, #)', receiver, startIndex, endIndex);
}
bool stringContainsStringUnchecked(
String receiver, String other, int startIndex) {
return stringIndexOfStringUnchecked(receiver, other, startIndex) >= 0;
}
List<String> stringSplitUnchecked(
String receiver,
/* String | JavaScript RegExp */
pattern) {
return new JSArray<String>.markGrowable(JS(
'returns:JSExtendableArray;new:true', '#.split(#)', receiver, pattern));
}
class StringMatch implements Match {
const StringMatch(int this.start, String this.input, String this.pattern);
int get end => start + pattern.length;
String operator [](int g) => group(g);
int get groupCount => 0;
String group(int group_) {
if (group_ != 0) {
throw new RangeError.value(group_);
}
return pattern;
}
List<String> groups(List<int> groups_) {
List<String> result = <String>[];
for (int g in groups_) {
result.add(group(g));
}
return result;
}
final int start;
final String input;
final String pattern;
}
Iterable<Match> allMatchesInStringUnchecked(
String pattern, String string, int startIndex) {
return new _StringAllMatchesIterable(string, pattern, startIndex);
}
class _StringAllMatchesIterable extends Iterable<Match> {
final String _input;
final String _pattern;
final int _index;
_StringAllMatchesIterable(this._input, this._pattern, this._index);
Iterator<Match> get iterator =>
new _StringAllMatchesIterator(_input, _pattern, _index);
Match get first {
int index = stringIndexOfStringUnchecked(_input, _pattern, _index);
if (index >= 0) {
return new StringMatch(index, _input, _pattern);
}
throw IterableElementError.noElement();
}
}
class _StringAllMatchesIterator implements Iterator<Match> {
final String _input;
final String _pattern;
int _index;
Match? _current;
_StringAllMatchesIterator(this._input, this._pattern, this._index);
bool moveNext() {
if (_index + _pattern.length > _input.length) {
_current = null;
return false;
}
int index = stringIndexOfStringUnchecked(_input, _pattern, _index);
if (index < 0) {
_index = _input.length + 1;
_current = null;
return false;
}
int end = index + _pattern.length;
_current = new StringMatch(index, _input, _pattern);
// Empty match, don't start at same location again.
if (end == _index) end++;
_index = end;
return true;
}
Match get current => _current!;
}
bool stringContainsUnchecked(String receiver, Pattern other, int startIndex) {
if (other is String) {
return stringContainsStringUnchecked(receiver, other, startIndex);
} else if (other is JSSyntaxRegExp) {
return other.hasMatch(receiver.substring(startIndex));
} else {
var substr = receiver.substring(startIndex);
return other.allMatches(substr).isNotEmpty;
}
}
String stringReplaceJS(String receiver, jsRegExp, String replacement) {
return JS('String', r'#.replace(#, #)', receiver, jsRegExp,
escapeReplacement(replacement));
}
String escapeReplacement(String replacement) {
// The JavaScript `String.prototype.replace` method recognizes replacement
// patterns in the replacement string. Dart does not have that behavior, so
// the replacement patterns need to be escaped.
// `String.prototype.replace` tends to be slower when there are replacement
// patterns, and the escaping itself uses replacement patterns, so it is
// worthwhile checking for `$` first.
if (stringContainsStringUnchecked(replacement, r'$', 0)) {
return JS('String', r'#.replace(/\$/g, "$$$$")', replacement);
}
return replacement;
}
String stringReplaceFirstRE(String receiver, JSSyntaxRegExp regexp,
String replacement, int startIndex) {
var match = regexp._execGlobal(receiver, startIndex);
if (match == null) return receiver;
var start = match.start;
var end = match.end;
return stringReplaceRangeUnchecked(receiver, start, end, replacement);
}
/// Returns a string for a RegExp pattern that matches [string]. This is done by
/// escaping all RegExp metacharacters.
quoteStringForRegExp(string) {
// We test and replace essentially the same RegExp because replacement when
// there are replacement patterns is slow enough to be worth avoiding.
if (JS('bool', r'/[[\]{}()*+?.\\^$|]/.test(#)', string)) {
return JS('String', r'#.replace(/[[\]{}()*+?.\\^$|]/g, "\\$&")', string);
}
return string;
}
String stringReplaceAllUnchecked(
String receiver, Pattern pattern, String replacement) {
checkString(replacement);
if (pattern is String) {
return stringReplaceAllUncheckedString(receiver, pattern, replacement);
}
if (pattern is JSSyntaxRegExp) {
var re = regExpGetGlobalNative(pattern);
return stringReplaceJS(receiver, re, replacement);
}
return stringReplaceAllGeneral(receiver, pattern, replacement);
}
String stringReplaceAllGeneral(
String receiver, Pattern pattern, String replacement) {
checkNull(pattern);
int startIndex = 0;
StringBuffer result = StringBuffer();
for (Match match in pattern.allMatches(receiver)) {
result.write(substring2Unchecked(receiver, startIndex, match.start));
result.write(replacement);
startIndex = match.end;
}
result.write(substring1Unchecked(receiver, startIndex));
return result.toString();
}
/// Replaces all non-overlapping occurences of [pattern] in [receiver] with
/// [replacement]. This should be replace with
/// (String.prototype.replaceAll)[https://github.com/tc39/proposal-string-replace-all]
/// when available.
String stringReplaceAllUncheckedString(
String receiver, String pattern, String replacement) {
if (pattern == "") {
if (receiver == "") {
return JS('String', '#', replacement); // help type inference.
}
StringBuffer result = new StringBuffer('');
int length = receiver.length;
result.write(replacement);
for (int i = 0; i < length; i++) {
result.write(receiver[i]);
result.write(replacement);
}
return result.toString();
}
if (!const bool.fromEnvironment(
'dart2js.testing.String.replaceAll.force.regexp')) {
// First check for no match.
int index = stringIndexOfStringUnchecked(receiver, pattern, 0);
if (index < 0) return receiver;
// The fastest approach in general is to replace with a global RegExp, but
// this requires the receiver string to be long enough to amortize the cost
// of creating the RegExp, and the replacement to have no '$' patterns,
// which tend to make `String.prototype.replace` much slower. In these
// cases, using split-join usually wins.
if (receiver.length < 500 ||
stringContainsStringUnchecked(replacement, r'$', 0)) {
return stringReplaceAllUsingSplitJoin(receiver, pattern, replacement);
}
}
var quoted = quoteStringForRegExp(pattern);
var replacer = JS('', "new RegExp(#, 'g')", quoted);
return stringReplaceJS(receiver, replacer, replacement);
}
String stringReplaceAllUsingSplitJoin(
String receiver, String pattern, String replacement) {
return JS('String', '#.split(#).join(#)', receiver, pattern, replacement);
}
String _matchString(Match match) => match[0]!;
String _stringIdentity(String string) => string;
String stringReplaceAllFuncUnchecked(String receiver, Pattern pattern,
String Function(Match)? onMatch, String Function(String)? onNonMatch) {
onMatch ??= _matchString;
onNonMatch ??= _stringIdentity;
if (pattern is String) {
return stringReplaceAllStringFuncUnchecked(
receiver, pattern, onMatch, onNonMatch);
}
// Placing the Pattern test here is indistinguishable from placing it at the
// top of the method but it saves an extra check on the `pattern is String`
// path.
if (pattern is! Pattern) {
throw new ArgumentError.value(pattern, 'pattern', 'is not a Pattern');
}
StringBuffer buffer = new StringBuffer('');
int startIndex = 0;
for (Match match in pattern.allMatches(receiver)) {
buffer.write(onNonMatch(receiver.substring(startIndex, match.start)));
buffer.write(onMatch(match));
startIndex = match.end;
}
buffer.write(onNonMatch(receiver.substring(startIndex)));
return buffer.toString();
}
String stringReplaceAllEmptyFuncUnchecked(String receiver,
String Function(Match) onMatch, String Function(String) onNonMatch) {
// Pattern is the empty string.
StringBuffer buffer = StringBuffer();
int length = receiver.length;
int i = 0;
buffer.write(onNonMatch(""));
while (i < length) {
buffer.write(onMatch(new StringMatch(i, receiver, "")));
// Special case to avoid splitting a surrogate pair.
int code = receiver.codeUnitAt(i);
if ((code & ~0x3FF) == 0xD800 && length > i + 1) {
// Leading surrogate;
code = receiver.codeUnitAt(i + 1);
if ((code & ~0x3FF) == 0xDC00) {
// Matching trailing surrogate.
buffer.write(onNonMatch(receiver.substring(i, i + 2)));
i += 2;
continue;
}
}
buffer.write(onNonMatch(receiver[i]));
i++;
}
buffer.write(onMatch(new StringMatch(i, receiver, "")));
buffer.write(onNonMatch(""));
return buffer.toString();
}
String stringReplaceAllStringFuncUnchecked(String receiver, String pattern,
String Function(Match) onMatch, String Function(String) onNonMatch) {
int patternLength = pattern.length;
if (patternLength == 0) {
return stringReplaceAllEmptyFuncUnchecked(receiver, onMatch, onNonMatch);
}
int length = receiver.length;
StringBuffer buffer = new StringBuffer('');
int startIndex = 0;
while (startIndex < length) {
int position = stringIndexOfStringUnchecked(receiver, pattern, startIndex);
if (position == -1) {
break;
}
buffer.write(onNonMatch(receiver.substring(startIndex, position)));
buffer.write(onMatch(new StringMatch(position, receiver, pattern)));
startIndex = position + patternLength;
}
buffer.write(onNonMatch(receiver.substring(startIndex)));
return buffer.toString();
}
String stringReplaceFirstUnchecked(
String receiver, Pattern pattern, String replacement, int startIndex) {
if (pattern is String) {
int index = stringIndexOfStringUnchecked(receiver, pattern, startIndex);
if (index < 0) return receiver;
int end = index + pattern.length;
return stringReplaceRangeUnchecked(receiver, index, end, replacement);
}
if (pattern is JSSyntaxRegExp) {
return startIndex == 0
? stringReplaceJS(receiver, regExpGetNative(pattern), replacement)
: stringReplaceFirstRE(receiver, pattern, replacement, startIndex);
}
checkNull(pattern);
Iterator<Match> matches = pattern.allMatches(receiver, startIndex).iterator;
if (!matches.moveNext()) return receiver;
Match match = matches.current;
return receiver.replaceRange(match.start, match.end, replacement);
}
String stringReplaceFirstMappedUnchecked(String receiver, Pattern pattern,
String Function(Match) replace, int startIndex) {
Iterator<Match> matches = pattern.allMatches(receiver, startIndex).iterator;
if (!matches.moveNext()) return receiver;
Match match = matches.current;
String replacement = "${replace(match)}";
return receiver.replaceRange(match.start, match.end, replacement);
}
String stringReplaceRangeUnchecked(
String receiver, int start, int end, String replacement) {
var prefix = JS('String', '#.substring(0, #)', receiver, start);
var suffix = JS('String', '#.substring(#)', receiver, end);
return "$prefix$replacement$suffix";
}