diff --git a/Jint.Tests.Test262/Test262Harness.settings.json b/Jint.Tests.Test262/Test262Harness.settings.json
index b3b0342cb2..b13a7cb000 100644
--- a/Jint.Tests.Test262/Test262Harness.settings.json
+++ b/Jint.Tests.Test262/Test262Harness.settings.json
@@ -17,7 +17,6 @@
"regexp-modifiers",
"regexp-unicode-property-escapes",
"regexp-v-flag",
- "RegExp.escape",
"source-phase-imports",
"tail-call-optimization",
"Temporal",
diff --git a/Jint/Native/RegExp/RegExpConstructor.cs b/Jint/Native/RegExp/RegExpConstructor.cs
index ce2295223d..ab19e8835e 100644
--- a/Jint/Native/RegExp/RegExpConstructor.cs
+++ b/Jint/Native/RegExp/RegExpConstructor.cs
@@ -1,3 +1,4 @@
+using System.Text;
using System.Text.RegularExpressions;
using Jint.Native.Function;
using Jint.Native.Object;
@@ -29,6 +30,15 @@ internal RegExpConstructor(
protected override void Initialize()
{
+ const PropertyFlag LengthFlags = PropertyFlag.Configurable;
+ const PropertyFlag PropertyFlags = PropertyFlag.Configurable | PropertyFlag.Writable;
+
+ var properties = new PropertyDictionary(1, checkExistingKeys: false)
+ {
+ ["escape"] = new PropertyDescriptor(new ClrFunction(Engine, "escape", Escape, 1, LengthFlags), PropertyFlags)
+ };
+ SetProperties(properties);
+
var symbols = new SymbolDictionary(1)
{
[GlobalSymbolRegistry.Species] = new GetSetPropertyDescriptor(get: new ClrFunction(_engine, "get [Symbol.species]", (thisObj, _) => thisObj, 0, PropertyFlag.Configurable), set: Undefined, PropertyFlag.Configurable)
@@ -36,6 +46,260 @@ protected override void Initialize()
SetSymbols(symbols);
}
+ ///
+ /// https://tc39.es/ecma262/#sec-regexp.escape
+ ///
+ private JsString Escape(JsValue thisObject, JsCallArguments arguments)
+ {
+ var s = arguments.At(0);
+
+ // 1. If S is not a String, throw a TypeError exception.
+ if (!s.IsString())
+ {
+ Throw.TypeError(_realm, "RegExp.escape requires a string argument");
+ return null!;
+ }
+
+ var str = s.ToString();
+
+ // 2. Let escaped be the empty String.
+ // 3. Let cpList be StringToCodePoints(S).
+ // 4. For each code point c in cpList, do
+ if (string.IsNullOrEmpty(str))
+ {
+ return JsString.Empty;
+ }
+
+ var sb = new ValueStringBuilder(stackalloc char[64]);
+ var isFirst = true;
+
+ // Iterate by code points (handling surrogate pairs)
+ for (var i = 0; i < str.Length; i++)
+ {
+ var c = str[i];
+ int codePoint;
+
+ // Handle surrogate pairs
+ if (char.IsHighSurrogate(c) && i + 1 < str.Length && char.IsLowSurrogate(str[i + 1]))
+ {
+ codePoint = char.ConvertToUtf32(c, str[i + 1]);
+ i++; // Skip the low surrogate
+ }
+ else
+ {
+ codePoint = c;
+ }
+
+ // a. If escaped is the empty String, and c is matched by DecimalDigit or AsciiLetter, then
+ if (isFirst && IsDecimalDigitOrAsciiLetter(codePoint))
+ {
+ // Escape as \xHH (lowercase hex)
+ sb.Append('\\');
+ sb.Append('x');
+ sb.Append(ToHexDigit((codePoint >> 4) & 0xF));
+ sb.Append(ToHexDigit(codePoint & 0xF));
+ }
+ else
+ {
+ // b. Set escaped to the string-concatenation of escaped and EncodeForRegExpEscape(c).
+ EncodeForRegExpEscape(ref sb, codePoint);
+ }
+
+ isFirst = false;
+ }
+
+ return new JsString(sb.ToString());
+ }
+
+ ///
+ /// https://tc39.es/ecma262/#sec-encodeforregexescape
+ ///
+ private static void EncodeForRegExpEscape(ref ValueStringBuilder sb, int codePoint)
+ {
+ // 1. If c is matched by SyntaxCharacter or c is U+002F (SOLIDUS), then
+ // a. Return the string-concatenation of 0x005C (REVERSE SOLIDUS) and UTF16EncodeCodePoint(c).
+ if (IsSyntaxCharacterOrSolidus(codePoint))
+ {
+ sb.Append('\\');
+ sb.Append((char) codePoint);
+ return;
+ }
+
+ // 2. If c is the code point listed in some cell of the "Code Point" column of Table 64, then
+ // a. Return the string-concatenation of 0x005C (REVERSE SOLIDUS) and the ControlEscape.
+ switch (codePoint)
+ {
+ case 0x09: // TAB
+ sb.Append('\\');
+ sb.Append('t');
+ return;
+ case 0x0A: // LF
+ sb.Append('\\');
+ sb.Append('n');
+ return;
+ case 0x0B: // VT
+ sb.Append('\\');
+ sb.Append('v');
+ return;
+ case 0x0C: // FF
+ sb.Append('\\');
+ sb.Append('f');
+ return;
+ case 0x0D: // CR
+ sb.Append('\\');
+ sb.Append('r');
+ return;
+ }
+
+ // 3-5. If c is in otherPunctuators, WhiteSpace, LineTerminator, or surrogate, escape it
+ if (ShouldEscape(codePoint))
+ {
+ if (codePoint <= 0xFF)
+ {
+ // \xHH format (2-digit lowercase hex)
+ sb.Append('\\');
+ sb.Append('x');
+ sb.Append(ToHexDigit((codePoint >> 4) & 0xF));
+ sb.Append(ToHexDigit(codePoint & 0xF));
+ }
+ else if (codePoint <= 0xFFFF)
+ {
+ // \uHHHH format (4-digit lowercase hex)
+ sb.Append('\\');
+ sb.Append('u');
+ sb.Append(ToHexDigit((codePoint >> 12) & 0xF));
+ sb.Append(ToHexDigit((codePoint >> 8) & 0xF));
+ sb.Append(ToHexDigit((codePoint >> 4) & 0xF));
+ sb.Append(ToHexDigit(codePoint & 0xF));
+ }
+ else
+ {
+ // Supplementary code point: encode as two \uHHHH escapes
+ var highSurrogate = (char) (0xD800 + ((codePoint - 0x10000) >> 10));
+ var lowSurrogate = (char) (0xDC00 + ((codePoint - 0x10000) & 0x3FF));
+
+ sb.Append('\\');
+ sb.Append('u');
+ sb.Append(ToHexDigit((highSurrogate >> 12) & 0xF));
+ sb.Append(ToHexDigit((highSurrogate >> 8) & 0xF));
+ sb.Append(ToHexDigit((highSurrogate >> 4) & 0xF));
+ sb.Append(ToHexDigit(highSurrogate & 0xF));
+
+ sb.Append('\\');
+ sb.Append('u');
+ sb.Append(ToHexDigit((lowSurrogate >> 12) & 0xF));
+ sb.Append(ToHexDigit((lowSurrogate >> 8) & 0xF));
+ sb.Append(ToHexDigit((lowSurrogate >> 4) & 0xF));
+ sb.Append(ToHexDigit(lowSurrogate & 0xF));
+ }
+ return;
+ }
+
+ // 6. Return UTF16EncodeCodePoint(c).
+ if (codePoint <= 0xFFFF)
+ {
+ sb.Append((char) codePoint);
+ }
+ else
+ {
+ // Supplementary code point: encode as surrogate pair
+ sb.Append((char) (0xD800 + ((codePoint - 0x10000) >> 10)));
+ sb.Append((char) (0xDC00 + ((codePoint - 0x10000) & 0x3FF)));
+ }
+ }
+
+ private static bool IsDecimalDigitOrAsciiLetter(int c)
+ {
+ return (c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z');
+ }
+
+ private static bool IsSyntaxCharacterOrSolidus(int c)
+ {
+ // SyntaxCharacter: . * + ? ^ $ | ( ) [ ] { } \
+ // Plus SOLIDUS: /
+ return c switch
+ {
+ '.' or '*' or '+' or '?' or '^' or '$' or '|' or '(' or ')' or '[' or ']' or '{' or '}' or '\\' or '/' => true,
+ _ => false
+ };
+ }
+
+ private static bool ShouldEscape(int c)
+ {
+ // OtherPunctuators: ,-=<>#&!%:;@~'`"
+ if (IsOtherPunctuator(c))
+ {
+ return true;
+ }
+
+ // WhiteSpace (excluding TAB, VT, FF which are handled as control escapes)
+ if (IsWhiteSpace(c))
+ {
+ return true;
+ }
+
+ // LineTerminator (excluding LF, CR which are handled as control escapes)
+ if (IsLineTerminator(c))
+ {
+ return true;
+ }
+
+ // Leading surrogate (0xD800-0xDBFF) or trailing surrogate (0xDC00-0xDFFF)
+ if (c >= 0xD800 && c <= 0xDFFF)
+ {
+ return true;
+ }
+
+ return false;
+ }
+
+ private static bool IsOtherPunctuator(int c)
+ {
+ // ,-=<>#&!%:;@~'`"
+ return c switch
+ {
+ ',' or '-' or '=' or '<' or '>' or '#' or '&' or '!' or '%' or ':' or ';' or '@' or '~' or '\'' or '`' or '"' => true,
+ _ => false
+ };
+ }
+
+ private static bool IsWhiteSpace(int c)
+ {
+ // WhiteSpace includes TAB, VT, FF, ZWNBSP, SPACE, NO-BREAK SPACE, and other USP
+ // TAB (0x09), VT (0x0B), FF (0x0C) are handled separately as control escapes
+ return c switch
+ {
+ 0x0009 or 0x000B or 0x000C => true, // Already handled but include for completeness
+ 0x0020 => true, // SPACE
+ 0x00A0 => true, // NO-BREAK SPACE
+ 0xFEFF => true, // ZWNBSP
+ 0x1680 => true, // OGHAM SPACE MARK
+ 0x2000 or 0x2001 or 0x2002 or 0x2003 or 0x2004 or 0x2005 or 0x2006 or 0x2007 or 0x2008 or 0x2009 or 0x200A => true, // Various spaces
+ 0x202F => true, // NARROW NO-BREAK SPACE
+ 0x205F => true, // MEDIUM MATHEMATICAL SPACE
+ 0x3000 => true, // IDEOGRAPHIC SPACE
+ _ => false
+ };
+ }
+
+ private static bool IsLineTerminator(int c)
+ {
+ // LineTerminator: LF, CR, LS, PS
+ // LF (0x0A) and CR (0x0D) are handled separately as control escapes
+ return c switch
+ {
+ 0x000A or 0x000D => true, // Already handled but include for completeness
+ 0x2028 => true, // LINE SEPARATOR
+ 0x2029 => true, // PARAGRAPH SEPARATOR
+ _ => false
+ };
+ }
+
+ private static char ToHexDigit(int value)
+ {
+ return (char) (value < 10 ? '0' + value : 'a' + (value - 10));
+ }
+
protected internal override JsValue Call(JsValue thisObject, JsCallArguments arguments)
{
return Construct(arguments, thisObject);
diff --git a/README.md b/README.md
index 1d305dee99..6c4a36269d 100644
--- a/README.md
+++ b/README.md
@@ -135,7 +135,7 @@ and many more.
- ✔ Iterator helper methods
- ✔ JSON modules
- ✔ `Promise.try`
-- ❌ `RegExp.escape()`
+- ✔ `RegExp.escape()`
- ❌ Regular expression pattern modifiers (inline flags)
- ❌ Duplicate named capture groups
- ✔ Set methods (`intersection`, `union`, `difference`, `symmetricDifference`, `isSubsetOf`, `isSupersetOf`, `isDisjointFrom`)