Index: src/js/string.js |
diff --git a/src/js/string.js b/src/js/string.js |
index 1da5c5f766b979353a5e053518f31f41cf7fe635..c0587350cd6d4730183398c094c1db517c3c5e3e 100644 |
--- a/src/js/string.js |
+++ b/src/js/string.js |
@@ -9,9 +9,21 @@ |
// ------------------------------------------------------------------- |
// Imports |
+var ArrayJoin; |
+var GlobalRegExp = global.RegExp; |
var GlobalString = global.String; |
+var MaxSimple; |
+var MinSimple; |
var matchSymbol = utils.ImportNow("match_symbol"); |
+var replaceSymbol = utils.ImportNow("replace_symbol"); |
var searchSymbol = utils.ImportNow("search_symbol"); |
+var splitSymbol = utils.ImportNow("split_symbol"); |
+ |
+utils.Import(function(from) { |
+ ArrayJoin = from.ArrayJoin; |
+ MaxSimple = from.MaxSimple; |
+ MinSimple = from.MinSimple; |
+}); |
//------------------------------------------------------------------- |
@@ -46,6 +58,154 @@ |
return regexp[matchSymbol](subject); |
} |
+// ES#sec-getsubstitution |
+// GetSubstitution(matched, str, position, captures, replacement) |
+// Expand the $-expressions in the string and return a new string with |
+// the result. |
+function GetSubstitution(matched, string, position, captures, replacement) { |
+ var matchLength = matched.length; |
+ var stringLength = string.length; |
+ var capturesLength = captures.length; |
+ var tailPos = position + matchLength; |
+ var result = ""; |
+ var pos, expansion, peek, next, scaledIndex, advance, newScaledIndex; |
+ |
+ var next = %StringIndexOf(replacement, '$', 0); |
+ if (next < 0) { |
+ result += replacement; |
+ return result; |
+ } |
+ |
+ if (next > 0) result += %_SubString(replacement, 0, next); |
+ |
+ while (true) { |
+ expansion = '$'; |
+ pos = next + 1; |
+ if (pos < replacement.length) { |
+ peek = %_StringCharCodeAt(replacement, pos); |
+ if (peek == 36) { // $$ |
+ ++pos; |
+ result += '$'; |
+ } else if (peek == 38) { // $& - match |
+ ++pos; |
+ result += matched; |
+ } else if (peek == 96) { // $` - prefix |
+ ++pos; |
+ result += %_SubString(string, 0, position); |
+ } else if (peek == 39) { // $' - suffix |
+ ++pos; |
+ result += %_SubString(string, tailPos, stringLength); |
+ } else if (peek >= 48 && peek <= 57) { |
+ // Valid indices are $1 .. $9, $01 .. $09 and $10 .. $99 |
+ scaledIndex = (peek - 48); |
+ advance = 1; |
+ if (pos + 1 < replacement.length) { |
+ next = %_StringCharCodeAt(replacement, pos + 1); |
+ if (next >= 48 && next <= 57) { |
+ newScaledIndex = scaledIndex * 10 + ((next - 48)); |
+ if (newScaledIndex < capturesLength) { |
+ scaledIndex = newScaledIndex; |
+ advance = 2; |
+ } |
+ } |
+ } |
+ if (scaledIndex != 0 && scaledIndex < capturesLength) { |
+ var capture = captures.at(scaledIndex); |
+ if (!IS_UNDEFINED(capture)) result += capture; |
+ pos += advance; |
+ } else { |
+ result += '$'; |
+ } |
+ } else { |
+ result += '$'; |
+ } |
+ } else { |
+ result += '$'; |
+ } |
+ |
+ // Go the the next $ in the replacement. |
+ next = %StringIndexOf(replacement, '$', pos); |
+ |
+ // Return if there are no more $ characters in the replacement. If we |
+ // haven't reached the end, we need to append the suffix. |
+ if (next < 0) { |
+ if (pos < replacement.length) { |
+ result += %_SubString(replacement, pos, replacement.length); |
+ } |
+ return result; |
+ } |
+ |
+ // Append substring between the previous and the next $ character. |
+ if (next > pos) { |
+ result += %_SubString(replacement, pos, next); |
+ } |
+ } |
+ return result; |
+} |
+ |
+// ES6, section 21.1.3.14 |
+function StringReplace(search, replace) { |
+ CHECK_OBJECT_COERCIBLE(this, "String.prototype.replace"); |
+ |
+ // Decision tree for dispatch |
+ // .. regexp search (in src/js/regexp.js, RegExpReplace) |
+ // .... string replace |
+ // ...... non-global search |
+ // ........ empty string replace |
+ // ........ non-empty string replace (with $-expansion) |
+ // ...... global search |
+ // ........ no need to circumvent last match info override |
+ // ........ need to circument last match info override |
+ // .... function replace |
+ // ...... global search |
+ // ...... non-global search |
+ // .. string search |
+ // .... special case that replaces with one single character |
+ // ...... function replace |
+ // ...... string replace (with $-expansion) |
+ |
+ if (!IS_NULL_OR_UNDEFINED(search)) { |
+ var replacer = search[replaceSymbol]; |
+ if (!IS_UNDEFINED(replacer)) { |
+ return %_Call(replacer, search, this, replace); |
+ } |
+ } |
+ |
+ var subject = TO_STRING(this); |
+ |
+ search = TO_STRING(search); |
+ |
+ if (search.length == 1 && |
+ subject.length > 0xFF && |
+ IS_STRING(replace) && |
+ %StringIndexOf(replace, '$', 0) < 0) { |
+ // Searching by traversing a cons string tree and replace with cons of |
+ // slices works only when the replaced string is a single character, being |
+ // replaced by a simple string and only pays off for long strings. |
+ return %StringReplaceOneCharWithString(subject, search, replace); |
+ } |
+ var start = %StringIndexOf(subject, search, 0); |
+ if (start < 0) return subject; |
+ var end = start + search.length; |
+ |
+ var result = %_SubString(subject, 0, start); |
+ |
+ // Compute the string to replace with. |
+ if (IS_CALLABLE(replace)) { |
+ result += replace(search, start, subject); |
+ } else { |
+ // In this case, we don't have any capture groups and can get away with |
+ // faking the captures object by simply setting its length to 1. |
+ const captures = { length: 1 }; |
+ const matched = %_SubString(subject, start, end); |
+ result += GetSubstitution(matched, subject, start, captures, |
+ TO_STRING(replace)); |
+ } |
+ |
+ return result + %_SubString(subject, end, subject.length); |
+} |
+ |
+ |
// ES6 21.1.3.15. |
function StringSearch(pattern) { |
CHECK_OBJECT_COERCIBLE(this, "String.prototype.search"); |
@@ -105,6 +265,39 @@ |
return %_SubString(s, start_i, end_i); |
} |
+ |
+ |
+// ES6 21.1.3.17. |
+function StringSplitJS(separator, limit) { |
+ CHECK_OBJECT_COERCIBLE(this, "String.prototype.split"); |
+ |
+ if (!IS_NULL_OR_UNDEFINED(separator)) { |
+ var splitter = separator[splitSymbol]; |
+ if (!IS_UNDEFINED(splitter)) { |
+ return %_Call(splitter, separator, this, limit); |
+ } |
+ } |
+ |
+ var subject = TO_STRING(this); |
+ limit = (IS_UNDEFINED(limit)) ? kMaxUint32 : TO_UINT32(limit); |
+ |
+ var length = subject.length; |
+ var separator_string = TO_STRING(separator); |
+ |
+ if (limit === 0) return []; |
+ |
+ // ECMA-262 says that if separator is undefined, the result should |
+ // be an array of size 1 containing the entire string. |
+ if (IS_UNDEFINED(separator)) return [subject]; |
+ |
+ var separator_length = separator_string.length; |
+ |
+ // If the separator string is empty then return the elements in the subject. |
+ if (separator_length === 0) return %StringToArray(subject, limit); |
+ |
+ return %StringSplit(subject, separator_string, limit); |
+} |
+ |
// ECMA-262, 15.5.4.16 |
function StringToLowerCaseJS() { |
@@ -322,8 +515,10 @@ |
"concat", StringConcat, |
"match", StringMatchJS, |
"repeat", StringRepeat, |
+ "replace", StringReplace, |
"search", StringSearch, |
"slice", StringSlice, |
+ "split", StringSplitJS, |
"toLowerCase", StringToLowerCaseJS, |
"toLocaleLowerCase", StringToLocaleLowerCase, |
"toUpperCase", StringToUpperCaseJS, |
@@ -344,4 +539,14 @@ |
"sup", StringSup |
]); |
+// ------------------------------------------------------------------- |
+// Exports |
+ |
+utils.Export(function(to) { |
+ to.StringMatch = StringMatchJS; |
+ to.StringReplace = StringReplace; |
+ to.StringSlice = StringSlice; |
+ to.StringSplit = StringSplitJS; |
+}); |
+ |
}) |