#pragma warning disable CA1859 // Use concrete types when possible for improved performance -- most of prototype methods return JsValue using System.Globalization; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text; using Jint.Native.Json; using Jint.Native.Object; using Jint.Native.RegExp; using Jint.Native.Symbol; using Jint.Runtime; using Jint.Runtime.Descriptors; using Jint.Runtime.Descriptors.Specialized; using Jint.Runtime.Interop; namespace Jint.Native.String; /// /// https://tc39.es/ecma262/#sec-properties-of-the-string-prototype-object /// internal sealed class StringPrototype : StringInstance { private readonly Realm _realm; private readonly StringConstructor _constructor; internal ClrFunction? _originalIteratorFunction; internal StringPrototype( Engine engine, Realm realm, StringConstructor constructor, ObjectPrototype objectPrototype) : base(engine, JsString.Empty) { _prototype = objectPrototype; _length = PropertyDescriptor.AllForbiddenDescriptor.NumberZero; _realm = realm; _constructor = constructor; } protected override void Initialize() { const PropertyFlag lengthFlags = PropertyFlag.Configurable; const PropertyFlag propertyFlags = lengthFlags | PropertyFlag.Writable; var trimStart = new LazyPropertyDescriptor(this, static prototype => new ClrFunction(prototype._engine, "trimStart", prototype.TrimStart, 0, lengthFlags), propertyFlags); var trimEnd = new LazyPropertyDescriptor(this, static prototype => new ClrFunction(prototype._engine, "trimEnd", prototype.TrimEnd, 0, lengthFlags), propertyFlags); var properties = new PropertyDictionary(37, checkExistingKeys: false) { ["constructor"] = new PropertyDescriptor(_constructor, PropertyFlag.NonEnumerable), ["toString"] = new LazyPropertyDescriptor(this, static prototype => new ClrFunction(prototype._engine, "toString", prototype.ToStringString, 0, lengthFlags), propertyFlags), ["valueOf"] = new LazyPropertyDescriptor(this, static prototype => new ClrFunction(prototype._engine, "valueOf", prototype.ValueOf, 0, lengthFlags), propertyFlags), ["charAt"] = new LazyPropertyDescriptor(this, static prototype => new ClrFunction(prototype._engine, "charAt", prototype.CharAt, 1, lengthFlags), propertyFlags), ["charCodeAt"] = new LazyPropertyDescriptor(this, static prototype => new ClrFunction(prototype._engine, "charCodeAt", prototype.CharCodeAt, 1, lengthFlags), propertyFlags), ["codePointAt"] = new LazyPropertyDescriptor(this, static prototype => new ClrFunction(prototype._engine, "codePointAt", prototype.CodePointAt, 1, lengthFlags), propertyFlags), ["concat"] = new LazyPropertyDescriptor(this, static prototype => new ClrFunction(prototype._engine, "concat", prototype.Concat, 1, lengthFlags), propertyFlags), ["indexOf"] = new LazyPropertyDescriptor(this, static prototype => new ClrFunction(prototype._engine, "indexOf", prototype.IndexOf, 1, lengthFlags), propertyFlags), ["endsWith"] = new LazyPropertyDescriptor(this, static prototype => new ClrFunction(prototype._engine, "endsWith", prototype.EndsWith, 1, lengthFlags), propertyFlags), ["startsWith"] = new LazyPropertyDescriptor(this, static prototype => new ClrFunction(prototype._engine, "startsWith", prototype.StartsWith, 1, lengthFlags), propertyFlags), ["lastIndexOf"] = new LazyPropertyDescriptor(this, static prototype => new ClrFunction(prototype._engine, "lastIndexOf", prototype.LastIndexOf, 1, lengthFlags), propertyFlags), ["localeCompare"] = new LazyPropertyDescriptor(this, static prototype => new ClrFunction(prototype._engine, "localeCompare", prototype.LocaleCompare, 1, lengthFlags), propertyFlags), ["match"] = new LazyPropertyDescriptor(this, static prototype => new ClrFunction(prototype._engine, "match", prototype.Match, 1, lengthFlags), propertyFlags), ["matchAll"] = new LazyPropertyDescriptor(this, static prototype => new ClrFunction(prototype._engine, "matchAll", prototype.MatchAll, 1, lengthFlags), propertyFlags), ["replace"] = new LazyPropertyDescriptor(this, static prototype => new ClrFunction(prototype._engine, "replace", prototype.Replace, 2, lengthFlags), propertyFlags), ["replaceAll"] = new LazyPropertyDescriptor(this, static prototype => new ClrFunction(prototype._engine, "replaceAll", prototype.ReplaceAll, 2, lengthFlags), propertyFlags), ["search"] = new LazyPropertyDescriptor(this, static prototype => new ClrFunction(prototype._engine, "search", prototype.Search, 1, lengthFlags), propertyFlags), ["slice"] = new LazyPropertyDescriptor(this, static prototype => new ClrFunction(prototype._engine, "slice", prototype.Slice, 2, lengthFlags), propertyFlags), ["split"] = new LazyPropertyDescriptor(this, static prototype => new ClrFunction(prototype._engine, "split", prototype.Split, 2, lengthFlags), propertyFlags), ["substr"] = new LazyPropertyDescriptor(this, static prototype => new ClrFunction(prototype._engine, "substr", Substr, 2), propertyFlags), ["substring"] = new LazyPropertyDescriptor(this, static prototype => new ClrFunction(prototype._engine, "substring", prototype.Substring, 2, lengthFlags), propertyFlags), ["toLowerCase"] = new LazyPropertyDescriptor(this, static prototype => new ClrFunction(prototype._engine, "toLowerCase", prototype.ToLowerCase, 0, lengthFlags), propertyFlags), ["toLocaleLowerCase"] = new LazyPropertyDescriptor(this, static prototype => new ClrFunction(prototype._engine, "toLocaleLowerCase", prototype.ToLocaleLowerCase, 0, lengthFlags), propertyFlags), ["toUpperCase"] = new LazyPropertyDescriptor(this, static prototype => new ClrFunction(prototype._engine, "toUpperCase", prototype.ToUpperCase, 0, lengthFlags), propertyFlags), ["toLocaleUpperCase"] = new LazyPropertyDescriptor(this, static prototype => new ClrFunction(prototype._engine, "toLocaleUpperCase", prototype.ToLocaleUpperCase, 0, lengthFlags), propertyFlags), ["trim"] = new LazyPropertyDescriptor(this, static prototype => new ClrFunction(prototype._engine, "trim", prototype.Trim, 0, lengthFlags), propertyFlags), ["trimStart"] = trimStart, ["trimEnd"] = trimEnd, ["trimLeft"] = trimStart, ["trimRight"] = trimEnd, ["padStart"] = new LazyPropertyDescriptor(this, static prototype => new ClrFunction(prototype._engine, "padStart", prototype.PadStart, 1, lengthFlags), propertyFlags), ["padEnd"] = new LazyPropertyDescriptor(this, static prototype => new ClrFunction(prototype._engine, "padEnd", prototype.PadEnd, 1, lengthFlags), propertyFlags), ["includes"] = new LazyPropertyDescriptor(this, static prototype => new ClrFunction(prototype._engine, "includes", prototype.Includes, 1, lengthFlags), propertyFlags), ["normalize"] = new LazyPropertyDescriptor(this, static prototype => new ClrFunction(prototype._engine, "normalize", prototype.Normalize, 0, lengthFlags), propertyFlags), ["repeat"] = new LazyPropertyDescriptor(this, static prototype => new ClrFunction(prototype._engine, "repeat", prototype.Repeat, 1, lengthFlags), propertyFlags), ["at"] = new LazyPropertyDescriptor(this, static prototype => new ClrFunction(prototype._engine, "at", prototype.At, 1, lengthFlags), propertyFlags), ["isWellFormed"] = new LazyPropertyDescriptor(this, static prototype => new ClrFunction(prototype._engine, "isWellFormed", prototype.IsWellFormed, 0, lengthFlags), propertyFlags), ["toWellFormed"] = new LazyPropertyDescriptor(this, static prototype => new ClrFunction(prototype._engine, "toWellFormed", prototype.ToWellFormed, 0, lengthFlags), propertyFlags), }; SetProperties(properties); _originalIteratorFunction = new ClrFunction(_engine, "[Symbol.iterator]", Iterator, 0, lengthFlags); var symbols = new SymbolDictionary(1) { [GlobalSymbolRegistry.Iterator] = new PropertyDescriptor(_originalIteratorFunction, propertyFlags) }; SetSymbols(symbols); } internal override bool HasOriginalIterator => ReferenceEquals(Get(GlobalSymbolRegistry.Iterator), _originalIteratorFunction); private ObjectInstance Iterator(JsValue thisObject, JsCallArguments arguments) { TypeConverter.RequireObjectCoercible(_engine, thisObject); var str = TypeConverter.ToString(thisObject); return _realm.Intrinsics.StringIteratorPrototype.Construct(str); } private JsValue ToStringString(JsValue thisObject, JsCallArguments arguments) { if (thisObject.IsString()) { return thisObject; } var s = TypeConverter.ToObject(_realm, thisObject) as StringInstance; if (s is null) { Throw.TypeError(_realm); } return s.StringData; } // http://msdn.microsoft.com/en-us/library/system.char.iswhitespace(v=vs.110).aspx // http://en.wikipedia.org/wiki/Byte_order_mark const char BOM_CHAR = '\uFEFF'; [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool IsWhiteSpaceEx(char c) { return char.IsWhiteSpace(c) || c == BOM_CHAR; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static string TrimEndEx(string s) { if (s.Length == 0) return string.Empty; if (!IsWhiteSpaceEx(s[s.Length - 1])) return s; return TrimEnd(s); } private static string TrimEnd(string s) { var i = s.Length - 1; while (i >= 0) { if (IsWhiteSpaceEx(s[i])) i--; else break; } return i >= 0 ? s.Substring(0, i + 1) : string.Empty; } [MethodImpl(MethodImplOptions.AggressiveInlining)] internal static string TrimStartEx(string s) { if (s.Length == 0) return string.Empty; if (!IsWhiteSpaceEx(s[0])) return s; return TrimStart(s); } private static string TrimStart(string s) { var i = 0; while (i < s.Length) { if (IsWhiteSpaceEx(s[i])) i++; else break; } return i >= s.Length ? string.Empty : s.Substring(i); } [MethodImpl(MethodImplOptions.AggressiveInlining)] internal static string TrimEx(string s) { return TrimEndEx(TrimStartEx(s)); } /// /// https://tc39.es/ecma262/#sec-string.prototype.trim /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private JsValue Trim(JsValue thisObject, JsCallArguments arguments) { TypeConverter.RequireObjectCoercible(Engine, thisObject); var s = TypeConverter.ToJsString(thisObject); if (s.Length == 0 || (!IsWhiteSpaceEx(s[0]) && !IsWhiteSpaceEx(s[s.Length - 1]))) { return s; } return TrimEx(s.ToString()); } /// /// https://tc39.es/ecma262/#sec-string.prototype.trimstart /// private JsValue TrimStart(JsValue thisObject, JsCallArguments arguments) { TypeConverter.RequireObjectCoercible(Engine, thisObject); var s = TypeConverter.ToJsString(thisObject); if (s.Length == 0 || !IsWhiteSpaceEx(s[0])) { return s; } return TrimStartEx(s.ToString()); } /// /// https://tc39.es/ecma262/#sec-string.prototype.trimend /// private JsValue TrimEnd(JsValue thisObject, JsCallArguments arguments) { TypeConverter.RequireObjectCoercible(Engine, thisObject); var s = TypeConverter.ToJsString(thisObject); if (s.Length == 0 || !IsWhiteSpaceEx(s[s.Length - 1])) { return s; } return TrimEndEx(s.ToString()); } private JsValue ToLocaleUpperCase(JsValue thisObject, JsCallArguments arguments) { TypeConverter.RequireObjectCoercible(_engine, thisObject); var s = TypeConverter.ToString(thisObject); var culture = CultureInfo.InvariantCulture; if (arguments.Length > 0 && arguments[0].IsString()) { try { var cultureArgument = arguments[0].ToString(); culture = CultureInfo.GetCultureInfo(cultureArgument); } catch (CultureNotFoundException) { Throw.RangeError(_realm, "Incorrect culture information provided"); } } if (string.Equals("lt", culture.Name, StringComparison.OrdinalIgnoreCase)) { s = StringInlHelper.LithuanianStringProcessor(s); #if NET462 // Code specific to .NET Framework 4.6.2. // For no good reason this verison does not upper case these characters correctly. return new JsString(s.ToUpper(culture) .Replace("ϳ", "Ϳ") .Replace("ʝ", "Ʝ")); #endif } return new JsString(s.ToUpper(culture)); } private JsValue ToUpperCase(JsValue thisObject, JsCallArguments arguments) { TypeConverter.RequireObjectCoercible(_engine, thisObject); var s = TypeConverter.ToString(thisObject); return new JsString(s.ToUpperInvariant()); } private JsValue ToLocaleLowerCase(JsValue thisObject, JsCallArguments arguments) { TypeConverter.RequireObjectCoercible(_engine, thisObject); var s = TypeConverter.ToString(thisObject); return new JsString(s.ToLower(CultureInfo.InvariantCulture)); } private JsValue ToLowerCase(JsValue thisObject, JsCallArguments arguments) { TypeConverter.RequireObjectCoercible(_engine, thisObject); var s = TypeConverter.ToString(thisObject); return s.ToLowerInvariant(); } private static int ToIntegerSupportInfinity(JsValue numberVal) { return numberVal._type == InternalTypes.Integer ? numberVal.AsInteger() : ToIntegerSupportInfinityUnlikely(numberVal); } [MethodImpl(MethodImplOptions.NoInlining)] private static int ToIntegerSupportInfinityUnlikely(JsValue numberVal) { var doubleVal = TypeConverter.ToInteger(numberVal); int intVal; if (double.IsPositiveInfinity(doubleVal)) intVal = int.MaxValue; else if (double.IsNegativeInfinity(doubleVal)) intVal = int.MinValue; else intVal = (int) doubleVal; return intVal; } private JsValue Substring(JsValue thisObject, JsCallArguments arguments) { TypeConverter.RequireObjectCoercible(Engine, thisObject); var s = TypeConverter.ToString(thisObject); var start = TypeConverter.ToNumber(arguments.At(0)); var end = TypeConverter.ToNumber(arguments.At(1)); if (double.IsNaN(start) || start < 0) { start = 0; } if (double.IsNaN(end) || end < 0) { end = 0; } var len = s.Length; var intStart = ToIntegerSupportInfinity(start); var intEnd = arguments.At(1).IsUndefined() ? len : ToIntegerSupportInfinity(end); var finalStart = System.Math.Min(len, System.Math.Max(intStart, 0)); var finalEnd = System.Math.Min(len, System.Math.Max(intEnd, 0)); // Swap value if finalStart < finalEnd var from = System.Math.Min(finalStart, finalEnd); var to = System.Math.Max(finalStart, finalEnd); var length = to - from; if (length == 0) { return JsString.Empty; } if (length == 1) { return JsString.Create(s[from]); } return new JsString(s.Substring(from, length)); } private static JsValue Substr(JsValue thisObject, JsCallArguments arguments) { var s = TypeConverter.ToString(thisObject); var start = TypeConverter.ToInteger(arguments.At(0)); var length = arguments.At(1).IsUndefined() ? double.PositiveInfinity : TypeConverter.ToInteger(arguments.At(1)); start = start >= 0 ? start : System.Math.Max(s.Length + start, 0); length = System.Math.Min(System.Math.Max(length, 0), s.Length - start); if (length <= 0) { return JsString.Empty; } var startIndex = TypeConverter.ToInt32(start); var l = TypeConverter.ToInt32(length); if (l == 1) { return TypeConverter.ToString(s[startIndex]); } return s.Substring(startIndex, l); } /// /// https://tc39.es/ecma262/#sec-string.prototype.split /// private JsValue Split(JsValue thisObject, JsCallArguments arguments) { TypeConverter.RequireObjectCoercible(Engine, thisObject); var separator = arguments.At(0); var limit = arguments.At(1); // fast path for empty regexp if (separator is JsRegExp R && string.Equals(R.Source, JsRegExp.regExpForMatchingAllCharacters, StringComparison.Ordinal)) { separator = JsString.Empty; } if (separator is ObjectInstance oi) { var splitter = GetMethod(_realm, oi, GlobalSymbolRegistry.Split); if (splitter != null) { return splitter.Call(separator, thisObject, limit); } } var s = TypeConverter.ToString(thisObject); // Coerce into a number, true will become 1 var lim = limit.IsUndefined() ? uint.MaxValue : TypeConverter.ToUint32(limit); if (separator.IsNull()) { separator = "null"; } else if (!separator.IsUndefined()) { if (!separator.IsRegExp()) { separator = TypeConverter.ToJsString(separator); // Coerce into a string, for an object call toString() } } if (lim == 0) { return _realm.Intrinsics.Array.ArrayCreate(0); } if (separator.IsUndefined()) { var arrayInstance = _realm.Intrinsics.Array.ArrayCreate(1); arrayInstance.SetIndexValue(0, s, updateLength: false); return arrayInstance; } return SplitWithStringSeparator(_realm, separator, s, lim); } internal static JsValue SplitWithStringSeparator(Realm realm, JsValue separator, string s, uint lim) { var segments = StringExecutionContext.Current.SplitSegmentList; segments.Clear(); var sep = TypeConverter.ToString(separator); if (sep == string.Empty) { if (s.Length > segments.Capacity) { segments.Capacity = s.Length; } for (var i = 0; i < s.Length; i++) { segments.Add(TypeConverter.ToString(s[i])); } } else { var array = StringExecutionContext.Current.SplitArray1; array[0] = sep; segments.AddRange(s.Split(array, StringSplitOptions.None)); } var length = (uint) System.Math.Min(segments.Count, lim); var a = realm.Intrinsics.Array.ArrayCreate(length); for (int i = 0; i < length; i++) { a.SetIndexValue((uint) i, segments[i], updateLength: false); } a.SetLength(length); return a; } /// /// https://tc39.es/proposal-relative-indexing-method/#sec-string-prototype-additions /// private JsValue At(JsValue thisObject, JsCallArguments arguments) { TypeConverter.RequireObjectCoercible(_engine, thisObject); var start = arguments.At(0); var o = thisObject.ToString(); long len = o.Length; var relativeIndex = TypeConverter.ToInteger(start); int k; if (relativeIndex < 0) { k = (int) (len + relativeIndex); } else { k = (int) relativeIndex; } if (k < 0 || k >= len) { return Undefined; } return o[k]; } private JsValue Slice(JsValue thisObject, JsCallArguments arguments) { TypeConverter.RequireObjectCoercible(Engine, thisObject); var start = TypeConverter.ToNumber(arguments.At(0)); if (double.IsNegativeInfinity(start)) { start = 0; } if (double.IsPositiveInfinity(start)) { return JsString.Empty; } var s = TypeConverter.ToJsString(thisObject); var end = TypeConverter.ToNumber(arguments.At(1)); if (double.IsPositiveInfinity(end)) { end = s.Length; } var len = s.Length; var intStart = (int) start; var intEnd = arguments.At(1).IsUndefined() ? len : (int) TypeConverter.ToInteger(end); var from = intStart < 0 ? System.Math.Max(len + intStart, 0) : System.Math.Min(intStart, len); var to = intEnd < 0 ? System.Math.Max(len + intEnd, 0) : System.Math.Min(intEnd, len); var span = System.Math.Max(to - from, 0); if (span == 0) { return JsString.Empty; } if (span == 1) { return JsString.Create(s[from]); } return s.Substring(from, span); } private JsValue Search(JsValue thisObject, JsCallArguments arguments) { TypeConverter.RequireObjectCoercible(Engine, thisObject); var regex = arguments.At(0); if (regex is ObjectInstance oi) { var searcher = GetMethod(_realm, oi, GlobalSymbolRegistry.Search); if (searcher != null) { return searcher.Call(regex, thisObject); } } var rx = (JsRegExp) _realm.Intrinsics.RegExp.Construct([regex]); var s = TypeConverter.ToJsString(thisObject); return _engine.Invoke(rx, GlobalSymbolRegistry.Search, [s]); } /// /// https://tc39.es/ecma262/#sec-string.prototype.replace /// private JsValue Replace(JsValue thisObject, JsCallArguments arguments) { TypeConverter.RequireObjectCoercible(Engine, thisObject); var searchValue = arguments.At(0); var replaceValue = arguments.At(1); if (!searchValue.IsNullOrUndefined()) { var replacer = GetMethod(_realm, searchValue, GlobalSymbolRegistry.Replace); if (replacer != null) { return replacer.Call(searchValue, thisObject, replaceValue); } } var thisString = TypeConverter.ToJsString(thisObject); var searchString = TypeConverter.ToString(searchValue); var functionalReplace = replaceValue is ICallable; if (!functionalReplace) { replaceValue = TypeConverter.ToJsString(replaceValue); } var position = thisString.IndexOf(searchString); if (position < 0) { return thisString; } string replStr; if (functionalReplace) { var replValue = ((ICallable) replaceValue).Call(Undefined, searchString, position, thisString); replStr = TypeConverter.ToString(replValue); } else { var captures = System.Array.Empty(); replStr = RegExpPrototype.GetSubstitution(searchString, thisString.ToString(), position, captures, Undefined, TypeConverter.ToString(replaceValue)); } var tailPos = position + searchString.Length; var newString = thisString.Substring(0, position) + replStr + thisString.Substring(tailPos); return newString; } /// /// https://tc39.es/ecma262/#sec-string.prototype.replaceall /// private JsValue ReplaceAll(JsValue thisObject, JsCallArguments arguments) { TypeConverter.RequireObjectCoercible(Engine, thisObject); var searchValue = arguments.At(0); var replaceValue = arguments.At(1); if (!searchValue.IsNullOrUndefined()) { if (searchValue.IsRegExp()) { var flags = searchValue.Get(RegExpPrototype.PropertyFlags); TypeConverter.RequireObjectCoercible(_engine, flags); if (!TypeConverter.ToString(flags).Contains('g')) { Throw.TypeError(_realm, "String.prototype.replaceAll called with a non-global RegExp argument"); } } var replacer = GetMethod(_realm, searchValue, GlobalSymbolRegistry.Replace); if (replacer != null) { return replacer.Call(searchValue, thisObject, replaceValue); } } var thisString = TypeConverter.ToString(thisObject); var searchString = TypeConverter.ToString(searchValue); var functionalReplace = replaceValue is ICallable; if (!functionalReplace) { replaceValue = TypeConverter.ToJsString(replaceValue); // check fast case var newValue = replaceValue.ToString(); if (!newValue.Contains('$') && searchString.Length > 0) { // just plain old string replace return thisString.Replace(searchString, newValue); } } // https://tc39.es/ecma262/#sec-stringindexof static int StringIndexOf(string s, string search, int fromIndex) { if (search.Length == 0 && fromIndex <= s.Length) { return fromIndex; } return fromIndex < s.Length ? s.IndexOf(search, fromIndex, StringComparison.Ordinal) : -1; } var searchLength = searchString.Length; var advanceBy = System.Math.Max(1, searchLength); var endOfLastMatch = 0; using var result = new ValueStringBuilder(); var position = StringIndexOf(thisString, searchString, 0); while (position != -1) { string replacement; var preserved = thisString.Substring(endOfLastMatch, position - endOfLastMatch); if (functionalReplace) { var replValue = ((ICallable) replaceValue).Call(Undefined, searchString, position, thisString); replacement = TypeConverter.ToString(replValue); } else { var captures = System.Array.Empty(); replacement = RegExpPrototype.GetSubstitution(searchString, thisString, position, captures, Undefined, TypeConverter.ToString(replaceValue)); } result.Append(preserved); result.Append(replacement); endOfLastMatch = position + searchLength; position = StringIndexOf(thisString, searchString, position + advanceBy); } if (endOfLastMatch < thisString.Length) { result.Append(thisString.AsSpan(endOfLastMatch)); } return result.ToString(); } private JsValue Match(JsValue thisObject, JsCallArguments arguments) { TypeConverter.RequireObjectCoercible(Engine, thisObject); var regex = arguments.At(0); if (regex is ObjectInstance oi) { var matcher = GetMethod(_realm, oi, GlobalSymbolRegistry.Match); if (matcher != null) { return matcher.Call(regex, thisObject); } } var rx = (JsRegExp) _realm.Intrinsics.RegExp.Construct([regex]); var s = TypeConverter.ToJsString(thisObject); return _engine.Invoke(rx, GlobalSymbolRegistry.Match, [s]); } private JsValue MatchAll(JsValue thisObject, JsCallArguments arguments) { TypeConverter.RequireObjectCoercible(_engine, thisObject); var regex = arguments.At(0); if (!regex.IsNullOrUndefined()) { if (regex.IsRegExp()) { var flags = regex.Get(RegExpPrototype.PropertyFlags); TypeConverter.RequireObjectCoercible(_engine, flags); if (!TypeConverter.ToString(flags).Contains('g')) { Throw.TypeError(_realm); } } var matcher = GetMethod(_realm, regex, GlobalSymbolRegistry.MatchAll); if (matcher != null) { return matcher.Call(regex, thisObject); } } var s = TypeConverter.ToJsString(thisObject); var rx = (JsRegExp) _realm.Intrinsics.RegExp.Construct([regex, "g"]); return _engine.Invoke(rx, GlobalSymbolRegistry.MatchAll, [s]); } private JsValue LocaleCompare(JsValue thisObject, JsCallArguments arguments) { TypeConverter.RequireObjectCoercible(Engine, thisObject); var s = TypeConverter.ToString(thisObject); var that = TypeConverter.ToString(arguments.At(0)); var culture = Engine.Options.Culture; if (arguments.Length > 1 && arguments[1].IsString()) { culture = CultureInfo.GetCultureInfo(arguments.At(1).AsString()); } return culture.CompareInfo.Compare(s.Normalize(NormalizationForm.FormKD), that.Normalize(NormalizationForm.FormKD)); } /// /// https://tc39.es/ecma262/#sec-string.prototype.lastindexof /// private JsValue LastIndexOf(JsValue thisObject, JsCallArguments arguments) { TypeConverter.RequireObjectCoercible(Engine, thisObject); var jsString = TypeConverter.ToJsString(thisObject); var searchStr = TypeConverter.ToString(arguments.At(0)); double numPos = double.NaN; if (arguments.Length > 1 && !arguments[1].IsUndefined()) { numPos = TypeConverter.ToNumber(arguments[1]); } var pos = double.IsNaN(numPos) ? double.PositiveInfinity : TypeConverter.ToInteger(numPos); var len = jsString.Length; var start = (int) System.Math.Min(System.Math.Max(pos, 0), len); var searchLen = searchStr.Length; if (searchLen > len) { return JsNumber.IntegerNegativeOne; } var s = jsString.ToString(); var i = start; bool found; do { found = true; var j = 0; while (found && j < searchLen) { if (i + searchLen > len || s[i + j] != searchStr[j]) { found = false; } else { j++; } } if (!found) { i--; } } while (!found && i >= 0); return i; } /// /// https://tc39.es/ecma262/#sec-string.prototype.indexof /// private JsValue IndexOf(JsValue thisObject, JsCallArguments arguments) { TypeConverter.RequireObjectCoercible(Engine, thisObject); var s = TypeConverter.ToJsString(thisObject); var searchStr = TypeConverter.ToString(arguments.At(0)); double pos = 0; if (arguments.Length > 1 && !arguments[1].IsUndefined()) { pos = TypeConverter.ToInteger(arguments[1]); } if (pos > s.Length) { pos = s.Length; } if (pos < 0) { pos = 0; } return s.IndexOf(searchStr, (int) pos); } private JsValue Concat(JsValue thisObject, JsCallArguments arguments) { TypeConverter.RequireObjectCoercible(Engine, thisObject); if (thisObject is not JsString jsString) { jsString = new JsString.ConcatenatedString(TypeConverter.ToString(thisObject)); } else { jsString = jsString.EnsureCapacity(0); } foreach (var argument in arguments) { jsString = jsString.Append(argument); } return jsString; } private JsValue CharCodeAt(JsValue thisObject, JsCallArguments arguments) { TypeConverter.RequireObjectCoercible(Engine, thisObject); JsValue pos = arguments.Length > 0 ? arguments[0] : 0; var s = TypeConverter.ToJsString(thisObject); var position = (int) TypeConverter.ToInteger(pos); if (position < 0 || position >= s.Length) { return JsNumber.DoubleNaN; } return (long) s[position]; } /// /// https://tc39.es/ecma262/#sec-string.prototype.codepointat /// private JsValue CodePointAt(JsValue thisObject, JsCallArguments arguments) { TypeConverter.RequireObjectCoercible(Engine, thisObject); JsValue pos = arguments.Length > 0 ? arguments[0] : 0; var s = TypeConverter.ToString(thisObject); var position = (int) TypeConverter.ToInteger(pos); if (position < 0 || position >= s.Length) { return Undefined; } return CodePointAt(s, position).CodePoint; } [StructLayout(LayoutKind.Auto)] private readonly record struct CodePointResult(int CodePoint, int CodeUnitCount, bool IsUnpairedSurrogate); private static CodePointResult CodePointAt(string s, int position) { var size = s.Length; var first = s.CharCodeAt(position); var cp = s.CharCodeAt(position); var firstIsLeading = char.IsHighSurrogate(first); var firstIsTrailing = char.IsLowSurrogate(first); if (!firstIsLeading && !firstIsTrailing) { return new CodePointResult(cp, 1, false); } if (firstIsTrailing || position + 1 == size) { return new CodePointResult(cp, 1, true); } var second = s.CharCodeAt(position + 1); if (!char.IsLowSurrogate(second)) { return new CodePointResult(cp, 1, true); } return new CodePointResult(char.ConvertToUtf32(first, second), 2, false); } private JsValue CharAt(JsValue thisObject, JsCallArguments arguments) { TypeConverter.RequireObjectCoercible(Engine, thisObject); var s = TypeConverter.ToJsString(thisObject); var position = TypeConverter.ToInteger(arguments.At(0)); var size = s.Length; if (position >= size || position < 0) { return JsString.Empty; } return JsString.Create(s[(int) position]); } private JsValue ValueOf(JsValue thisObject, JsCallArguments arguments) { if (thisObject is StringInstance si) { return si.StringData; } if (thisObject is JsString) { return thisObject; } Throw.TypeError(_realm); return Undefined; } /// /// https://tc39.es/ecma262/#sec-string.prototype.padstart /// private JsValue PadStart(JsValue thisObject, JsCallArguments arguments) { return StringPad(thisObject, arguments, true); } /// /// https://tc39.es/ecma262/#sec-string.prototype.padend /// private JsValue PadEnd(JsValue thisObject, JsCallArguments arguments) { return StringPad(thisObject, arguments, false); } /// /// https://tc39.es/ecma262/#sec-stringpad /// private JsValue StringPad(JsValue thisObject, JsCallArguments arguments, bool padStart) { TypeConverter.RequireObjectCoercible(Engine, thisObject); var s = TypeConverter.ToJsString(thisObject); var targetLength = TypeConverter.ToInt32(arguments.At(0)); var padStringValue = arguments.At(1); var padString = padStringValue.IsUndefined() ? " " : TypeConverter.ToString(padStringValue); if (s.Length > targetLength || padString.Length == 0) { return s; } targetLength -= s.Length; if (targetLength > padString.Length) { padString = string.Join("", System.Linq.Enumerable.Repeat(padString, (targetLength / padString.Length) + 1)); } return padStart ? $"{padString.Substring(0, targetLength)}{s}" : $"{s}{padString.Substring(0, targetLength)}"; } /// /// https://tc39.es/ecma262/#sec-string.prototype.startswith /// private JsValue StartsWith(JsValue thisObject, JsCallArguments arguments) { TypeConverter.RequireObjectCoercible(Engine, thisObject); var s = TypeConverter.ToJsString(thisObject); var searchString = arguments.At(0); if (ReferenceEquals(searchString, Null)) { searchString = "null"; } else { if (searchString.IsRegExp()) { Throw.TypeError(_realm); } } var searchStr = TypeConverter.ToString(searchString); var pos = TypeConverter.ToInt32(arguments.At(1)); var len = s.Length; var start = System.Math.Min(System.Math.Max(pos, 0), len); return s.StartsWith(searchStr, start); } /// /// https://tc39.es/ecma262/#sec-string.prototype.endswith /// private JsValue EndsWith(JsValue thisObject, JsCallArguments arguments) { TypeConverter.RequireObjectCoercible(Engine, thisObject); var s = TypeConverter.ToJsString(thisObject); var searchString = arguments.At(0); if (ReferenceEquals(searchString, Null)) { searchString = "null"; } else { if (searchString.IsRegExp()) { Throw.TypeError(_realm); } } var searchStr = TypeConverter.ToString(searchString); var len = s.Length; var pos = TypeConverter.ToInt32(arguments.At(1, len)); var end = System.Math.Min(System.Math.Max(pos, 0), len); return s.EndsWith(searchStr, end); } /// /// https://tc39.es/ecma262/#sec-string.prototype.includes /// private JsValue Includes(JsValue thisObject, JsCallArguments arguments) { TypeConverter.RequireObjectCoercible(Engine, thisObject); var s = TypeConverter.ToJsString(thisObject); var searchString = arguments.At(0); if (searchString.IsRegExp()) { Throw.TypeError(_realm, "First argument to String.prototype.includes must not be a regular expression"); } var searchStr = TypeConverter.ToString(searchString); double pos = 0; if (arguments.Length > 1 && !arguments[1].IsUndefined()) { pos = TypeConverter.ToInteger(arguments[1]); } if (searchStr.Length == 0) { return JsBoolean.True; } if (pos < 0) { pos = 0; } return s.IndexOf(searchStr, (int) pos) > -1; } private JsValue Normalize(JsValue thisObject, JsCallArguments arguments) { TypeConverter.RequireObjectCoercible(Engine, thisObject); var str = TypeConverter.ToString(thisObject); var param = arguments.At(0); var form = "NFC"; if (!param.IsUndefined()) { form = TypeConverter.ToString(param); } var nf = NormalizationForm.FormC; switch (form) { case "NFC": nf = NormalizationForm.FormC; break; case "NFD": nf = NormalizationForm.FormD; break; case "NFKC": nf = NormalizationForm.FormKC; break; case "NFKD": nf = NormalizationForm.FormKD; break; default: Throw.RangeError( _realm, "The normalization form should be one of NFC, NFD, NFKC, NFKD."); break; } return str.Normalize(nf); } /// /// https://tc39.es/ecma262/#sec-string.prototype.repeat /// private JsValue Repeat(JsValue thisObject, JsCallArguments arguments) { TypeConverter.RequireObjectCoercible(Engine, thisObject); var s = TypeConverter.ToString(thisObject); var count = arguments.At(0); var n = TypeConverter.ToIntegerOrInfinity(count); if (n < 0 || double.IsPositiveInfinity(n)) { Throw.RangeError(_realm, "Invalid count value"); } if (n == 0 || s.Length == 0) { return JsString.Empty; } if (s.Length == 1) { return new string(s[0], (int) n); } var sb = new ValueStringBuilder((int) (n * s.Length)); for (var i = 0; i < n; ++i) { sb.Append(s); } return sb.ToString(); } private JsValue IsWellFormed(JsValue thisObject, JsCallArguments arguments) { TypeConverter.RequireObjectCoercible(_engine, thisObject); var s = TypeConverter.ToString(thisObject); return IsStringWellFormedUnicode(s); } private JsValue ToWellFormed(JsValue thisObject, JsCallArguments arguments) { TypeConverter.RequireObjectCoercible(_engine, thisObject); var s = TypeConverter.ToString(thisObject); var strLen = s.Length; var k = 0; var result = new ValueStringBuilder(); while (k < strLen) { var cp = CodePointAt(s, k); if (cp.IsUnpairedSurrogate) { // \uFFFD result.Append('�'); } else { result.Append(s.AsSpan(k, cp.CodeUnitCount)); } k += cp.CodeUnitCount; } return result.ToString(); } private static bool IsStringWellFormedUnicode(string s) { for (var i = 0; i < s.Length; ++i) { var isSurrogate = (s.CharCodeAt(i) & 0xF800) == 0xD800; if (!isSurrogate) { continue; } var isLeadingSurrogate = s.CharCodeAt(i) < 0xDC00; if (!isLeadingSurrogate) { return false; // unpaired trailing surrogate } var isFollowedByTrailingSurrogate = i + 1 < s.Length && (s.CharCodeAt(i + 1) & 0xFC00) == 0xDC00; if (!isFollowedByTrailingSurrogate) { return false; // unpaired leading surrogate } ++i; } return true; } }