| /* |
| * Copyright (C) 2016-2018 Apple Inc. All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions |
| * are met: |
| * 1. Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * 2. Redistributions in binary form must reproduce the above copyright |
| * notice, this list of conditions and the following disclaimer in the |
| * documentation and/or other materials provided with the distribution. |
| * |
| * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY |
| * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE |
| * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
| * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR |
| * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
| * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
| * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
| * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY |
| * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| @globalPrivate |
| @constructor |
| function RegExpStringIterator(regExp, string, global, fullUnicode) |
| { |
| "use strict"; |
| |
| @putByIdDirectPrivate(this, "regExpStringIteratorRegExp", regExp); |
| @putByIdDirectPrivate(this, "regExpStringIteratorString", string); |
| @putByIdDirectPrivate(this, "regExpStringIteratorGlobal", global); |
| @putByIdDirectPrivate(this, "regExpStringIteratorUnicode", fullUnicode); |
| @putByIdDirectPrivate(this, "regExpStringIteratorDone", false); |
| } |
| |
| @globalPrivate |
| function advanceStringIndex(string, index, unicode) |
| { |
| // This function implements AdvanceStringIndex described in ES6 21.2.5.2.3. |
| "use strict"; |
| |
| if (!unicode) |
| return index + 1; |
| |
| if (index + 1 >= string.length) |
| return index + 1; |
| |
| var first = string.@charCodeAt(index); |
| if (first < 0xD800 || first > 0xDBFF) |
| return index + 1; |
| |
| var second = string.@charCodeAt(index + 1); |
| if (second < 0xDC00 || second > 0xDFFF) |
| return index + 1; |
| |
| return index + 2; |
| } |
| |
| @globalPrivate |
| function regExpExec(regexp, str) |
| { |
| "use strict"; |
| |
| var exec = regexp.exec; |
| var builtinExec = @regExpBuiltinExec; |
| if (exec !== builtinExec && @isCallable(exec)) { |
| var result = exec.@call(regexp, str); |
| if (result !== null && !@isObject(result)) |
| @throwTypeError("The result of a RegExp exec must be null or an object"); |
| return result; |
| } |
| return builtinExec.@call(regexp, str); |
| } |
| |
| @globalPrivate |
| function hasObservableSideEffectsForRegExpMatch(regexp) |
| { |
| "use strict"; |
| |
| if (!@isRegExpObject(regexp)) |
| return true; |
| |
| // This is accessed by the RegExpExec internal function. |
| var regexpExec = @tryGetById(regexp, "exec"); |
| if (regexpExec !== @regExpBuiltinExec) |
| return true; |
| |
| var regexpGlobal = @tryGetById(regexp, "global"); |
| if (regexpGlobal !== @regExpProtoGlobalGetter) |
| return true; |
| var regexpUnicode = @tryGetById(regexp, "unicode"); |
| if (regexpUnicode !== @regExpProtoUnicodeGetter) |
| return true; |
| |
| return typeof regexp.lastIndex !== "number"; |
| } |
| |
| @globalPrivate |
| function matchSlow(regexp, str) |
| { |
| "use strict"; |
| |
| if (!regexp.global) |
| return @regExpExec(regexp, str); |
| |
| var unicode = regexp.unicode; |
| regexp.lastIndex = 0; |
| var resultList = []; |
| |
| // FIXME: It would be great to implement a solution similar to what we do in |
| // RegExpObject::matchGlobal(). It's not clear if this is possible, since this loop has |
| // effects. https://bugs.webkit.org/show_bug.cgi?id=158145 |
| var maximumReasonableMatchSize = 100000000; |
| |
| while (true) { |
| var result = @regExpExec(regexp, str); |
| |
| if (result === null) { |
| if (resultList.length === 0) |
| return null; |
| return resultList; |
| } |
| |
| if (resultList.length > maximumReasonableMatchSize) |
| @throwOutOfMemoryError(); |
| |
| var resultString = @toString(result[0]); |
| |
| if (!resultString.length) |
| regexp.lastIndex = @advanceStringIndex(str, regexp.lastIndex, unicode); |
| |
| @arrayPush(resultList, resultString); |
| } |
| } |
| |
| @overriddenName="[Symbol.match]" |
| function match(strArg) |
| { |
| "use strict"; |
| |
| if (!@isObject(this)) |
| @throwTypeError("RegExp.prototype.@@match requires that |this| be an Object"); |
| |
| var str = @toString(strArg); |
| |
| // Check for observable side effects and call the fast path if there aren't any. |
| if (!@hasObservableSideEffectsForRegExpMatch(this)) |
| return @regExpMatchFast.@call(this, str); |
| return @matchSlow(this, str); |
| } |
| |
| @overriddenName="[Symbol.matchAll]" |
| function matchAll(strArg) |
| { |
| "use strict"; |
| |
| var regExp = this; |
| if (!@isObject(regExp)) |
| @throwTypeError("RegExp.prototype.@@matchAll requires |this| to be an Object"); |
| |
| var string = @toString(strArg); |
| var Matcher = @speciesConstructor(regExp, @RegExp); |
| |
| var flags = @toString(regExp.flags); |
| var matcher = new Matcher(regExp, flags); |
| matcher.lastIndex = @toLength(regExp.lastIndex); |
| |
| var global = @stringIncludesInternal.@call(flags, "g"); |
| var fullUnicode = @stringIncludesInternal.@call(flags, "u"); |
| |
| return new @RegExpStringIterator(matcher, string, global, fullUnicode); |
| } |
| |
| @globalPrivate |
| function getSubstitution(matched, str, position, captures, namedCaptures, replacement) |
| { |
| "use strict"; |
| |
| var matchLength = matched.length; |
| var stringLength = str.length; |
| var tailPos = position + matchLength; |
| var m = captures.length; |
| var replacementLength = replacement.length; |
| var result = ""; |
| var lastStart = 0; |
| |
| for (var start = 0; start = @stringIndexOfInternal.@call(replacement, "$", lastStart), start !== -1; lastStart = start) { |
| if (start - lastStart > 0) |
| result = result + @stringSubstringInternal.@call(replacement, lastStart, start); |
| start++; |
| if (start >= replacementLength) |
| result = result + "$"; |
| else { |
| var ch = replacement[start]; |
| switch (ch) |
| { |
| case "$": |
| result = result + "$"; |
| start++; |
| break; |
| case "&": |
| result = result + matched; |
| start++; |
| break; |
| case "`": |
| if (position > 0) |
| result = result + @stringSubstringInternal.@call(str, 0, position); |
| start++; |
| break; |
| case "'": |
| if (tailPos < stringLength) |
| result = result + @stringSubstringInternal.@call(str, tailPos); |
| start++; |
| break; |
| case "<": |
| if (namedCaptures !== @undefined) { |
| var groupNameStartIndex = start + 1; |
| var groupNameEndIndex = @stringIndexOfInternal.@call(replacement, ">", groupNameStartIndex); |
| if (groupNameEndIndex !== -1) { |
| var groupName = @stringSubstringInternal.@call(replacement, groupNameStartIndex, groupNameEndIndex); |
| var capture = namedCaptures[groupName]; |
| if (capture !== @undefined) |
| result = result + @toString(capture); |
| |
| start = groupNameEndIndex + 1; |
| break; |
| } |
| } |
| |
| result = result + "$<"; |
| start++; |
| break; |
| default: |
| var chCode = ch.@charCodeAt(0); |
| if (chCode >= 0x30 && chCode <= 0x39) { |
| var originalStart = start - 1; |
| start++; |
| |
| var n = chCode - 0x30; |
| if (n > m) { |
| result = result + @stringSubstringInternal.@call(replacement, originalStart, start); |
| break; |
| } |
| |
| if (start < replacementLength) { |
| var nextChCode = replacement.@charCodeAt(start); |
| if (nextChCode >= 0x30 && nextChCode <= 0x39) { |
| var nn = 10 * n + nextChCode - 0x30; |
| if (nn <= m) { |
| n = nn; |
| start++; |
| } |
| } |
| } |
| |
| if (n == 0) { |
| result = result + @stringSubstringInternal.@call(replacement, originalStart, start); |
| break; |
| } |
| |
| var capture = captures[n - 1]; |
| if (capture !== @undefined) |
| result = result + capture; |
| } else |
| result = result + "$"; |
| break; |
| } |
| } |
| } |
| |
| return result + @stringSubstringInternal.@call(replacement, lastStart); |
| } |
| |
| @overriddenName="[Symbol.replace]" |
| function replace(strArg, replace) |
| { |
| "use strict"; |
| |
| if (!@isObject(this)) |
| @throwTypeError("RegExp.prototype.@@replace requires that |this| be an Object"); |
| |
| var regexp = this; |
| |
| var str = @toString(strArg); |
| var stringLength = str.length; |
| var functionalReplace = @isCallable(replace); |
| |
| if (!functionalReplace) |
| replace = @toString(replace); |
| |
| var global = regexp.global; |
| var unicode = false; |
| |
| if (global) { |
| unicode = regexp.unicode; |
| regexp.lastIndex = 0; |
| } |
| |
| var resultList = []; |
| var result; |
| var done = false; |
| while (!done) { |
| result = @regExpExec(regexp, str); |
| |
| if (result === null) |
| done = true; |
| else { |
| @arrayPush(resultList, result); |
| if (!global) |
| done = true; |
| else { |
| var matchStr = @toString(result[0]); |
| |
| if (!matchStr.length) { |
| var thisIndex = @toLength(regexp.lastIndex); |
| regexp.lastIndex = @advanceStringIndex(str, thisIndex, unicode); |
| } |
| } |
| } |
| } |
| |
| var accumulatedResult = ""; |
| var nextSourcePosition = 0; |
| |
| for (var i = 0, resultListLength = resultList.length; i < resultListLength; ++i) { |
| var result = resultList[i]; |
| var nCaptures = result.length - 1; |
| if (nCaptures < 0) |
| nCaptures = 0; |
| var matched = @toString(result[0]); |
| var matchLength = matched.length; |
| var position = @toIntegerOrInfinity(result.index); |
| position = (position > stringLength) ? stringLength : position; |
| position = (position < 0) ? 0 : position; |
| |
| var captures = []; |
| for (var n = 1; n <= nCaptures; n++) { |
| var capN = result[n]; |
| if (capN !== @undefined) |
| capN = @toString(capN); |
| @arrayPush(captures, capN); |
| } |
| |
| var replacement; |
| var namedCaptures = result.groups; |
| |
| if (functionalReplace) { |
| var replacerArgs = [ matched ]; |
| for (var j = 0; j < captures.length; j++) |
| @arrayPush(replacerArgs, captures[j]); |
| |
| @arrayPush(replacerArgs, position); |
| @arrayPush(replacerArgs, str); |
| |
| if (namedCaptures !== @undefined) |
| @arrayPush(replacerArgs, namedCaptures); |
| |
| var replValue = replace.@apply(@undefined, replacerArgs); |
| replacement = @toString(replValue); |
| } else { |
| if (namedCaptures !== @undefined) |
| namedCaptures = @toObject(namedCaptures, "RegExp.prototype[Symbol.replace] requires 'groups' property of a match not be null"); |
| |
| replacement = @getSubstitution(matched, str, position, captures, namedCaptures, replace); |
| } |
| |
| if (position >= nextSourcePosition) { |
| accumulatedResult = accumulatedResult + @stringSubstringInternal.@call(str, nextSourcePosition, position) + replacement; |
| nextSourcePosition = position + matchLength; |
| } |
| } |
| |
| if (nextSourcePosition >= stringLength) |
| return accumulatedResult; |
| |
| return accumulatedResult + @stringSubstringInternal.@call(str, nextSourcePosition); |
| } |
| |
| // 21.2.5.9 RegExp.prototype[@@search] (string) |
| @overriddenName="[Symbol.search]" |
| function search(strArg) |
| { |
| "use strict"; |
| |
| var regexp = this; |
| |
| // Check for observable side effects and call the fast path if there aren't any. |
| if (@isRegExpObject(regexp) |
| && @tryGetById(regexp, "exec") === @regExpBuiltinExec |
| && typeof regexp.lastIndex === "number") |
| return @regExpSearchFast.@call(regexp, strArg); |
| |
| // 1. Let rx be the this value. |
| // 2. If Type(rx) is not Object, throw a TypeError exception. |
| if (!@isObject(this)) |
| @throwTypeError("RegExp.prototype.@@search requires that |this| be an Object"); |
| |
| // 3. Let S be ? ToString(string). |
| var str = @toString(strArg) |
| |
| // 4. Let previousLastIndex be ? Get(rx, "lastIndex"). |
| var previousLastIndex = regexp.lastIndex; |
| |
| // 5. If SameValue(previousLastIndex, 0) is false, then |
| // 5.a. Perform ? Set(rx, "lastIndex", 0, true). |
| if (!@sameValue(previousLastIndex, 0)) |
| regexp.lastIndex = 0; |
| |
| // 6. Let result be ? RegExpExec(rx, S). |
| var result = @regExpExec(regexp, str); |
| |
| // 7. Let currentLastIndex be ? Get(rx, "lastIndex"). |
| // 8. If SameValue(currentLastIndex, previousLastIndex) is false, then |
| // 8.a. Perform ? Set(rx, "lastIndex", previousLastIndex, true). |
| if (!@sameValue(regexp.lastIndex, previousLastIndex)) |
| regexp.lastIndex = previousLastIndex; |
| |
| // 9. If result is null, return -1. |
| if (result === null) |
| return -1; |
| |
| // 10. Return ? Get(result, "index"). |
| return result.index; |
| } |
| |
| @globalPrivate |
| function hasObservableSideEffectsForRegExpSplit(regexp) |
| { |
| "use strict"; |
| |
| if (!@isRegExpObject(regexp)) |
| return true; |
| |
| // This is accessed by the RegExpExec internal function. |
| var regexpExec = @tryGetById(regexp, "exec"); |
| if (regexpExec !== @regExpBuiltinExec) |
| return true; |
| |
| // This is accessed by step 5 below. |
| var regexpFlags = @tryGetById(regexp, "flags"); |
| if (regexpFlags !== @regExpProtoFlagsGetter) |
| return true; |
| |
| // These are accessed by the builtin flags getter. |
| var regexpGlobal = @tryGetById(regexp, "global"); |
| if (regexpGlobal !== @regExpProtoGlobalGetter) |
| return true; |
| var regexpIgnoreCase = @tryGetById(regexp, "ignoreCase"); |
| if (regexpIgnoreCase !== @regExpProtoIgnoreCaseGetter) |
| return true; |
| var regexpMultiline = @tryGetById(regexp, "multiline"); |
| if (regexpMultiline !== @regExpProtoMultilineGetter) |
| return true; |
| var regexpSticky = @tryGetById(regexp, "sticky"); |
| if (regexpSticky !== @regExpProtoStickyGetter) |
| return true; |
| var regexpUnicode = @tryGetById(regexp, "unicode"); |
| if (regexpUnicode !== @regExpProtoUnicodeGetter) |
| return true; |
| |
| // These are accessed by the RegExp species constructor. |
| var regexpSource = @tryGetById(regexp, "source"); |
| if (regexpSource !== @regExpProtoSourceGetter) |
| return true; |
| var regexpSymbolMatch = @tryGetByIdWithWellKnownSymbol(regexp, "match"); |
| if (regexpSymbolMatch !== @regExpPrototypeSymbolMatch) |
| return true; |
| |
| return typeof regexp.lastIndex !== "number"; |
| } |
| |
| // ES 21.2.5.11 RegExp.prototype[@@split](string, limit) |
| @overriddenName="[Symbol.split]" |
| function split(string, limit) |
| { |
| "use strict"; |
| |
| // 1. Let rx be the this value. |
| // 2. If Type(rx) is not Object, throw a TypeError exception. |
| if (!@isObject(this)) |
| @throwTypeError("RegExp.prototype.@@split requires that |this| be an Object"); |
| var regexp = this; |
| |
| // 3. Let S be ? ToString(string). |
| var str = @toString(string); |
| |
| // 4. Let C be ? SpeciesConstructor(rx, %RegExp%). |
| var speciesConstructor = @speciesConstructor(regexp, @RegExp); |
| |
| if (speciesConstructor === @RegExp && !@hasObservableSideEffectsForRegExpSplit(regexp)) |
| return @regExpSplitFast.@call(regexp, str, limit); |
| |
| // 5. Let flags be ? ToString(? Get(rx, "flags")). |
| var flags = @toString(regexp.flags); |
| |
| // 6. If flags contains "u", var unicodeMatching be true. |
| // 7. Else, let unicodeMatching be false. |
| var unicodeMatching = @stringIncludesInternal.@call(flags, "u"); |
| // 8. If flags contains "y", var newFlags be flags. |
| // 9. Else, let newFlags be the string that is the concatenation of flags and "y". |
| var newFlags = @stringIncludesInternal.@call(flags, "y") ? flags : flags + "y"; |
| |
| // 10. Let splitter be ? Construct(C, « rx, newFlags »). |
| var splitter = new speciesConstructor(regexp, newFlags); |
| |
| // We need to check again for RegExp subclasses that will fail the speciesConstructor test |
| // but can still use the fast path after we invoke the constructor above. |
| if (!@hasObservableSideEffectsForRegExpSplit(splitter)) |
| return @regExpSplitFast.@call(splitter, str, limit); |
| |
| // 11. Let A be ArrayCreate(0). |
| // 12. Let lengthA be 0. |
| var result = []; |
| |
| // 13. If limit is undefined, let lim be 2^32-1; else var lim be ? ToUint32(limit). |
| limit = (limit === @undefined) ? 0xffffffff : limit >>> 0; |
| |
| // 16. If lim = 0, return A. |
| if (!limit) |
| return result; |
| |
| // 14. [Defered from above] Let size be the number of elements in S. |
| var size = str.length; |
| |
| // 17. If size = 0, then |
| if (!size) { |
| // a. Let z be ? RegExpExec(splitter, S). |
| var z = @regExpExec(splitter, str); |
| // b. If z is not null, return A. |
| if (z !== null) |
| return result; |
| // c. Perform ! CreateDataProperty(A, "0", S). |
| @putByValDirect(result, 0, str); |
| // d. Return A. |
| return result; |
| } |
| |
| // 15. [Defered from above] Let p be 0. |
| var position = 0; |
| // 18. Let q be p. |
| var matchPosition = 0; |
| |
| // 19. Repeat, while q < size |
| while (matchPosition < size) { |
| // a. Perform ? Set(splitter, "lastIndex", q, true). |
| splitter.lastIndex = matchPosition; |
| // b. Let z be ? RegExpExec(splitter, S). |
| var matches = @regExpExec(splitter, str); |
| // c. If z is null, let q be AdvanceStringIndex(S, q, unicodeMatching). |
| if (matches === null) |
| matchPosition = @advanceStringIndex(str, matchPosition, unicodeMatching); |
| // d. Else z is not null, |
| else { |
| // i. Let e be ? ToLength(? Get(splitter, "lastIndex")). |
| var endPosition = @toLength(splitter.lastIndex); |
| // ii. Let e be min(e, size). |
| endPosition = (endPosition <= size) ? endPosition : size; |
| // iii. If e = p, let q be AdvanceStringIndex(S, q, unicodeMatching). |
| if (endPosition === position) |
| matchPosition = @advanceStringIndex(str, matchPosition, unicodeMatching); |
| // iv. Else e != p, |
| else { |
| // 1. Let T be a String value equal to the substring of S consisting of the elements at indices p (inclusive) through q (exclusive). |
| var subStr = @stringSubstringInternal.@call(str, position, matchPosition); |
| // 2. Perform ! CreateDataProperty(A, ! ToString(lengthA), T). |
| // 3. Let lengthA be lengthA + 1. |
| @arrayPush(result, subStr); |
| // 4. If lengthA = lim, return A. |
| if (result.length == limit) |
| return result; |
| |
| // 5. Let p be e. |
| position = endPosition; |
| // 6. Let numberOfCaptures be ? ToLength(? Get(z, "length")). |
| // 7. Let numberOfCaptures be max(numberOfCaptures-1, 0). |
| var numberOfCaptures = matches.length > 1 ? matches.length - 1 : 0; |
| |
| // 8. Let i be 1. |
| var i = 1; |
| // 9. Repeat, while i <= numberOfCaptures, |
| while (i <= numberOfCaptures) { |
| // a. Let nextCapture be ? Get(z, ! ToString(i)). |
| var nextCapture = matches[i]; |
| // b. Perform ! CreateDataProperty(A, ! ToString(lengthA), nextCapture). |
| // d. Let lengthA be lengthA + 1. |
| @arrayPush(result, nextCapture); |
| // e. If lengthA = lim, return A. |
| if (result.length == limit) |
| return result; |
| // c. Let i be i + 1. |
| i++; |
| } |
| // 10. Let q be p. |
| matchPosition = position; |
| } |
| } |
| } |
| // 20. Let T be a String value equal to the substring of S consisting of the elements at indices p (inclusive) through size (exclusive). |
| var remainingStr = @stringSubstringInternal.@call(str, position, size); |
| // 21. Perform ! CreateDataProperty(A, ! ToString(lengthA), T). |
| @arrayPush(result, remainingStr); |
| // 22. Return A. |
| return result; |
| } |
| |
| // ES 21.2.5.13 RegExp.prototype.test(string) |
| @intrinsic=RegExpTestIntrinsic |
| function test(strArg) |
| { |
| "use strict"; |
| |
| var regexp = this; |
| |
| // Check for observable side effects and call the fast path if there aren't any. |
| if (@isRegExpObject(regexp) |
| && @tryGetById(regexp, "exec") === @regExpBuiltinExec |
| && typeof regexp.lastIndex === "number") |
| return @regExpTestFast.@call(regexp, strArg); |
| |
| // 1. Let R be the this value. |
| // 2. If Type(R) is not Object, throw a TypeError exception. |
| if (!@isObject(regexp)) |
| @throwTypeError("RegExp.prototype.test requires that |this| be an Object"); |
| |
| // 3. Let string be ? ToString(S). |
| var str = @toString(strArg); |
| |
| // 4. Let match be ? RegExpExec(R, string). |
| var match = @regExpExec(regexp, str); |
| |
| // 5. If match is not null, return true; else return false. |
| if (match !== null) |
| return true; |
| return false; |
| } |