Skip to content

Commit 4f6ac11

Browse files
Adds RegExp.escape (#98)
This PR adds a polyfill for `RegExp.escape`. As part of this, it refactors `String.prototype.padStart` and `String.prototype.padEnd` to use a new `StringPad` abstract operation. --------- Co-authored-by: Romain Menke <11521496+romainmenke@users.noreply.github.com>
1 parent 8d6feec commit 4f6ac11

16 files changed

Lines changed: 352 additions & 30 deletions

File tree

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
aliases = [ "es2025" ]
2+
dependencies = [
3+
"_ESAbstract.CreateMethodProperty",
4+
"_ESAbstract.EncodeForRegExpEscape",
5+
"_ESAbstract.StringToCodePoints",
6+
]
7+
spec = "https://tc39.es/proposal-regex-escaping/#sec-regexp.escape"
8+
docs = "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/escape"
9+
10+
[browsers]
11+
android = "*"
12+
chrome = "<136"
13+
edge = "<136"
14+
edge_mob = "<136"
15+
firefox = "<134"
16+
firefox_mob = "<134"
17+
ie = "*"
18+
ie_mob = "*"
19+
opera = "<121"
20+
op_mob = "<90"
21+
safari = "<18.2"
22+
ios_saf = "<18.2"
23+
samsung_mob = "<29.0"

polyfills/RegExp/escape/detect.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"escape" in RegExp;
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/* global CreateMethodProperty, EncodeForRegExpEscape, StringToCodePoints */
2+
// 22.2.5.1 RegExp.escape ( S )
3+
CreateMethodProperty(RegExp, "escape", function escape(S) {
4+
// 1. If S is not a String, throw a TypeError exception.
5+
if (typeof S !== "string") {
6+
throw new TypeError("S must be a string");
7+
}
8+
// 2. Let escaped be the empty String.
9+
var escaped = "";
10+
// 3. Let cpList be StringToCodePoints(S).
11+
var cpList = StringToCodePoints(S);
12+
// 4. For each code point c of cpList, do
13+
for (var i = 0; i < cpList.length; i++) {
14+
var c = cpList[i];
15+
// a. If escaped is the empty String and c is matched by either DecimalDigit or AsciiLetter, then
16+
if (
17+
escaped === "" &&
18+
((c >= 0x0030 && c <= 0x0039) || // 0-9
19+
(c >= 0x0041 && c <= 0x005a) || // A-Z
20+
(c >= 0x0061 && c <= 0x007a)) // a-z
21+
) {
22+
// i. NOTE: Escaping a leading digit ensures that output corresponds with pattern text which may be used after a \0 character escape or a DecimalEscape such as \1 and still match S rather than be interpreted as an extension of the preceding escape sequence. Escaping a leading ASCII letter does the same for the context after \c.
23+
// ii. Let numericValue be the numeric value of c.
24+
var numericValue = c;
25+
// iii. Let hex be Number::toString(𝔽(numericValue), 16).
26+
var hex = Number.prototype.toString.call(numericValue, 16);
27+
// iv. Assert: The length of hex is 2.
28+
// v. Set escaped to the string-concatenation of the code unit 0x005C (REVERSE SOLIDUS), "x", and hex.
29+
escaped = "\\x" + hex;
30+
}
31+
// b. Else,
32+
else {
33+
// i. Set escaped to the string-concatenation of escaped and EncodeForRegExpEscape(c).
34+
escaped += EncodeForRegExpEscape(c);
35+
}
36+
}
37+
// 5. Return escaped.
38+
return escaped;
39+
});
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
it("is a function", function () {
2+
proclaim.isFunction(RegExp.escape);
3+
});
4+
5+
it("has correct arity", function () {
6+
proclaim.arity(RegExp.escape, 1);
7+
});
8+
9+
it("has correct name", function () {
10+
proclaim.hasName(RegExp.escape, "escape");
11+
});
12+
13+
it("is not enumerable", function () {
14+
proclaim.isNotEnumerable(RegExp, "escape");
15+
});
16+
17+
describe("escape", function () {
18+
it("escapes special regex characters", function () {
19+
proclaim.strictEqual(RegExp.escape("10$"), "\\x310\\$");
20+
proclaim.strictEqual(RegExp.escape("abcdefg_123456"), "\\x61bcdefg_123456");
21+
proclaim.strictEqual(RegExp.escape("Привет"), "Привет");
22+
proclaim.strictEqual(
23+
RegExp.escape("(){}[]|,.?*+-^$=<>\\/#&!%:;@~'\"`"),
24+
"\\(\\)\\{\\}\\[\\]\\|\\x2c\\.\\?\\*\\+\\x2d\\^\\$\\x3d\\x3c\\x3e\\\\\\/\\x23\\x26\\x21\\x25\\x3a\\x3b\\x40\\x7e\\x27\\x22\\x60"
25+
);
26+
proclaim.strictEqual(
27+
RegExp.escape(
28+
"\u0009\u000A\u000B\u000C\u000D\u0020\u00A0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u202F\u205F\u3000\u2028\u2029\uFEFF"
29+
),
30+
"\\t\\n\\v\\f\\r\\x20\\xa0\\u1680\\u2000\\u2001\\u2002\\u2003\\u2004\\u2005\\u2006\\u2007\\u2008\\u2009\\u200a\\u202f\\u205f\\u3000\\u2028\\u2029\\ufeff"
31+
);
32+
33+
proclaim.strictEqual(RegExp.escape("💩"), "💩");
34+
proclaim.strictEqual(RegExp.escape("\uD83D"), "\\ud83d");
35+
proclaim.strictEqual(RegExp.escape("\uDCA9"), "\\udca9");
36+
proclaim.strictEqual(RegExp.escape("\uDCA9\uD83D"), "\\udca9\\ud83d");
37+
});
38+
39+
it("throws on non-string input", function () {
40+
proclaim.throws(function () {
41+
RegExp.escape(42);
42+
}, TypeError);
43+
proclaim.throws(function () {
44+
RegExp.escape({});
45+
}, TypeError);
46+
});
47+
});

polyfills/String/prototype/padEnd/config.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ aliases = [ "es7", "es2016", "es2017" ]
22
dependencies = [
33
"_ESAbstract.CreateMethodProperty",
44
"_ESAbstract.RequireObjectCoercible",
5+
"_ESAbstract.StringPad",
56
"_ESAbstract.ToString",
67
"_ESAbstract.ToLength"
78
]

polyfills/String/prototype/padEnd/polyfill.js

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/* global CreateMethodProperty, RequireObjectCoercible, ToLength, ToString */
1+
/* global CreateMethodProperty, RequireObjectCoercible, StringPad, ToLength, ToString */
22
// 21.1.3.13. String.prototype.padEnd( maxLength [ , fillString ] )
33
CreateMethodProperty(String.prototype, 'padEnd', function padEnd(maxLength /* [ , fillString ] */) {
44
'use strict';
@@ -22,18 +22,6 @@ CreateMethodProperty(String.prototype, 'padEnd', function padEnd(maxLength /* [
2222
} else {
2323
filler = ToString(fillString);
2424
}
25-
// 8. If filler is the empty String, return S.
26-
if (filler === '') {
27-
return S;
28-
}
29-
// 9. Let fillLen be intMaxLength - stringLength.
30-
var fillLen = intMaxLength - stringLength;
31-
// 10. Let truncatedStringFiller be the String value consisting of repeated concatenations of filler truncated to length fillLen.
32-
var truncatedStringFiller = '';
33-
for (var i = 0; i < fillLen; i++) {
34-
truncatedStringFiller += filler;
35-
}
36-
truncatedStringFiller = truncatedStringFiller.substr(0, fillLen);
37-
// 11. Return the string-concatenation of S and truncatedStringFiller.
38-
return S + truncatedStringFiller;
25+
// 8. Return StringPad(S, intMaxLength, filler, end).
26+
return StringPad(S, intMaxLength, filler, "END");
3927
});

polyfills/String/prototype/padStart/config.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ aliases = [ "es7", "es2016", "es2017" ]
22
dependencies = [
33
"_ESAbstract.CreateMethodProperty",
44
"_ESAbstract.RequireObjectCoercible",
5+
"_ESAbstract.StringPad",
56
"_ESAbstract.ToString",
67
"_ESAbstract.ToLength"
78
]

polyfills/String/prototype/padStart/polyfill.js

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/* global CreateMethodProperty, RequireObjectCoercible, ToLength, ToString */
1+
/* global CreateMethodProperty, RequireObjectCoercible, StringPad, ToLength, ToString */
22
// 21.1.3.14. String.prototype.padStart( maxLength [ , fillString ] )
33
CreateMethodProperty(String.prototype, 'padStart', function padStart(maxLength /* [ , fillString ] */) {
44
'use strict';
@@ -22,18 +22,6 @@ CreateMethodProperty(String.prototype, 'padStart', function padStart(maxLength /
2222
} else {
2323
filler = ToString(fillString);
2424
}
25-
// 8. If filler is the empty String, return S.
26-
if (filler === '') {
27-
return S;
28-
}
29-
// 9. Let fillLen be intMaxLength - stringLength.
30-
var fillLen = intMaxLength - stringLength;
31-
// 10. Let truncatedStringFiller be the String value consisting of repeated concatenations of filler truncated to length fillLen.
32-
var truncatedStringFiller = '';
33-
for (var i = 0; i < fillLen; i++) {
34-
truncatedStringFiller += filler;
35-
}
36-
truncatedStringFiller = truncatedStringFiller.substr(0, fillLen);
37-
// 11. Return the string-concatenation of truncatedStringFiller and S.
38-
return truncatedStringFiller + S;
25+
// 8. Return StringPad(S, intMaxLength, filler, start).
26+
return StringPad(S, intMaxLength, filler, "START");
3927
});
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
dependencies = [
2+
"_ESAbstract.StringPad",
3+
"_ESAbstract.StringToCodePoints",
4+
"_ESAbstract.UTF16EncodeCodePoint",
5+
"_ESAbstract.UnicodeEscape",
6+
]
7+
spec = "https://tc39.es/proposal-regex-escaping/#sec-encodeforregexpescape"
8+
9+
[browsers]
10+
android = "*"
11+
bb = "*"
12+
chrome = "*"
13+
edge = "*"
14+
edge_mob = "*"
15+
firefox = "*"
16+
firefox_mob = "*"
17+
ie = "*"
18+
ie_mob = "*"
19+
opera = "*"
20+
op_mob = "*"
21+
op_mini = "*"
22+
safari = "*"
23+
ios_saf = "*"
24+
samsung_mob = "*"
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/* global StringPad, StringToCodePoints, UTF16EncodeCodePoint, UnicodeEscape */
2+
// 22.2.5.1.1 EncodeForRegExpEscape ( c )
3+
// eslint-disable-next-line no-unused-vars
4+
function EncodeForRegExpEscape(c) {
5+
// 1. If c is matched by SyntaxCharacter or c is U+002F (SOLIDUS), then
6+
if (
7+
c === 0x005e || // ^
8+
c === 0x0024 || // $
9+
c === 0x005c || // \
10+
c === 0x002e || // .
11+
c === 0x002a || // *
12+
c === 0x002b || // +
13+
c === 0x003f || // ?
14+
c === 0x0028 || // (
15+
c === 0x0029 || // )
16+
c === 0x005b || // [
17+
c === 0x005d || // ]
18+
c === 0x007b || // {
19+
c === 0x007d || // }
20+
c === 0x007c || // |
21+
c === 0x002f // /
22+
) {
23+
// a. Return the string-concatenation of 0x005C (REVERSE SOLIDUS) and UTF16EncodeCodePoint(c).
24+
return "\\" + UTF16EncodeCodePoint(c);
25+
}
26+
// 2. Else if c is the code point listed in some cell of the “Code Point” column of Table 63, then
27+
else if (c >= 0x0009 && c <= 0x000d) {
28+
// a. Return the string-concatenation of 0x005C (REVERSE SOLIDUS) and the string in the “ControlEscape” column of the row whose “Code Point” column contains c.
29+
return (
30+
"\\" +
31+
{
32+
0x0009: "t", // CHARACTER TABULATION
33+
0x000a: "n", // LINE FEED
34+
0x000b: "v", // LINE TABULATION
35+
0x000c: "f", // FORM FEED
36+
0x000d: "r" // CARRIAGE RETURN
37+
}[c]
38+
);
39+
}
40+
// 3. Let otherPunctuators be the string-concatenation of ",-=<>#&!%:;@~'`" and the code unit 0x0022 (QUOTATION MARK).
41+
var otherPunctuators = ",-=<>#&!%:;@~'`\"";
42+
// 4. Let toEscape be StringToCodePoints(otherPunctuators).
43+
var toEscape = StringToCodePoints(otherPunctuators);
44+
// 5. If toEscape contains c, c is matched by either WhiteSpace or LineTerminator, or c has the same numeric value as a leading surrogate or trailing surrogate, then
45+
if (
46+
toEscape.indexOf(c) > -1 ||
47+
// https://www.compart.com/en/unicode/category/Zs
48+
c === 0xfeff || // ZERO WIDTH NO-BREAK SPACE
49+
c === 0x0020 || // SPACE
50+
c === 0x00a0 || // NO-BREAK SPACE
51+
c === 0x1680 || // OGHAM SPACE MARK
52+
(c >= 0x2000 && c <= 0x200a) || // other spaces
53+
c === 0x202f || // NARROW NO-BREAK SPACE
54+
c === 0x205f || // MEDIUM MATHEMATICAL SPACE
55+
c === 0x3000 || // IDEOGRAPHIC SPACE
56+
c === 0x2028 || // LINE SEPARATOR
57+
c === 0x2029 || // PARAGRAPH SEPARATOR
58+
(c >= 0xd800 && c <= 0xdbff) || // leading surrogate
59+
(c >= 0xdc00 && c <= 0xdfff) // trailing surrogate
60+
) {
61+
// a. Let cNum be the numeric value of c.
62+
var cNum = c;
63+
// b. If cNum ≤ 0xFF, then
64+
if (cNum <= 0x00ff) {
65+
// i. Let hex be Number::toString(𝔽(cNum), 16).
66+
var hex = Number.prototype.toString.call(cNum, 16);
67+
// ii. Return the string-concatenation of the code unit 0x005C (REVERSE SOLIDUS), "x", and StringPad(hex, 2, "0", start).
68+
return "\\x" + StringPad(hex, 2, "0", "START");
69+
}
70+
// c. Let escaped be the empty String.
71+
var escaped = "";
72+
// d. Let codeUnits be UTF16EncodeCodePoint(c).
73+
var codeUnits = UTF16EncodeCodePoint(c);
74+
// e. For each code unit cu of codeUnits, do
75+
for (var i = 0; i < codeUnits.length; i++) {
76+
var cu = codeUnits[i];
77+
// i. Set escaped to the string-concatenation of escaped and UnicodeEscape(cu).
78+
escaped += UnicodeEscape(cu);
79+
}
80+
// f. Return escaped.
81+
return escaped;
82+
}
83+
// 6. Return UTF16EncodeCodePoint(c).
84+
return UTF16EncodeCodePoint(c);
85+
}

0 commit comments

Comments
 (0)