Skip to content

Commit b6675dc

Browse files
balajirraogbrail
authored andcommitted
Introduce AbstractEcmaStringOperations.ReplacementOperation
This is to avoid repeatedly parsing the replacement template in case of multiple matches.
1 parent 29aba18 commit b6675dc

3 files changed

Lines changed: 275 additions & 79 deletions

File tree

Lines changed: 247 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,46 @@
11
package org.mozilla.javascript;
22

3+
import java.util.ArrayList;
4+
import java.util.List;
5+
36
/** Abstract operations for string manipulation as defined by EcmaScript */
47
public class AbstractEcmaStringOperations {
5-
/**
6-
* GetSubstitution(matched, str, position, captures, namedCaptures, replacementTemplate)
7-
*
8-
* <p><a
9-
* href="https://tc39.es/ecma262/multipage/text-processing.html#sec-getsubstitution">22.1.3.19.1
10-
* GetSubstitution (matched, str, position, captures, namedCaptures, replacementTemplate)</a>
11-
*/
12-
public static String getSubstitution(
13-
Context cx,
14-
Scriptable scope,
15-
String matched,
16-
String str,
17-
int position,
18-
NativeArray capturesArray,
19-
Object namedCaptures,
20-
String replacementTemplate) {
21-
// See ECMAScript spec 22.1.3.19.1
22-
int stringLength = str.length();
23-
if (position > stringLength) Kit.codeBug();
24-
StringBuilder result = new StringBuilder();
25-
String templateRemainder = replacementTemplate;
26-
while (!templateRemainder.isEmpty()) {
27-
String ref = templateRemainder.substring(0, 1);
28-
String refReplacement = ref;
29-
30-
if (templateRemainder.charAt(0) == '$') {
31-
if (templateRemainder.length() > 1) {
32-
char c = templateRemainder.charAt(1);
8+
9+
public static List<ReplacementOperation> buildReplacementList(String replacementTemplate) {
10+
List<ReplacementOperation> ops = new ArrayList<>();
11+
int position = 0;
12+
int start = 0;
13+
14+
while (position < replacementTemplate.length()) {
15+
if (replacementTemplate.charAt(position) == '$') {
16+
if (position < (replacementTemplate.length() - 1)) {
17+
if (start != position) {
18+
ops.add(
19+
new LiteralReplacement(
20+
replacementTemplate.substring(start, position)));
21+
}
22+
String ref = replacementTemplate.substring(position, position + 1);
23+
char c = replacementTemplate.charAt(position + 1);
3324
switch (c) {
3425
case '$':
3526
ref = "$$";
36-
refReplacement = "$";
27+
ops.add(new LiteralReplacement("$"));
3728
break;
3829

3930
case '`':
4031
ref = "$`";
41-
refReplacement = str.substring(0, position);
32+
ops.add(new FromStartToMatchReplacement());
4233
break;
4334

4435
case '&':
4536
ref = "$&";
46-
refReplacement = matched;
37+
ops.add(new MatchedReplacement());
4738
break;
4839

4940
case '\'':
5041
{
5142
ref = "$'";
52-
int matchLength = matched.length();
53-
int tailPos = position + matchLength;
54-
refReplacement = str.substring(Math.min(tailPos, stringLength));
43+
ops.add(new FromMatchToEndReplacement());
5544
break;
5645
}
5746

@@ -67,66 +56,78 @@ public static String getSubstitution(
6756
case '9':
6857
{
6958
int digitCount = 1;
70-
if (templateRemainder.length() > 2) {
71-
char c2 = templateRemainder.charAt(2);
59+
if (replacementTemplate.length() > position + 2) {
60+
char c2 = replacementTemplate.charAt(position + 2);
7261
if (isAsciiDigit(c2)) {
7362
digitCount = 2;
7463
}
7564
}
76-
String digits = templateRemainder.substring(1, 1 + digitCount);
65+
String digits =
66+
replacementTemplate.substring(
67+
position + 1, position + 1 + digitCount);
68+
ref =
69+
replacementTemplate.substring(
70+
position, position + 1 + digitCount);
7771

7872
// No need for ScriptRuntime version; we know the string is one or
7973
// two characters and
8074
// contains only [0-9]
8175
int index = Integer.parseInt(digits);
82-
long captureLen = capturesArray.getLength();
83-
if (index > captureLen && digitCount == 2) {
84-
digitCount = 1;
85-
digits = digits.substring(0, 1);
86-
index = Integer.parseInt(digits);
87-
}
88-
ref = templateRemainder.substring(0, 1 + digitCount);
89-
if (1 <= index && index <= captureLen) {
90-
Object capture = capturesArray.get(index - 1);
91-
if (capture
92-
== null) { // Undefined or missing are returned as null
93-
refReplacement = "";
94-
} else {
95-
refReplacement = ScriptRuntime.toString(capture);
96-
}
76+
if (digits.length() == 1) {
77+
ops.add(new OneDigitCaptureReplacement(index));
9778
} else {
98-
refReplacement = ref;
79+
ops.add(new TwoDigitCaptureReplacement(index));
9980
}
10081
break;
10182
}
10283

10384
case '<':
10485
{
105-
int gtPos = templateRemainder.indexOf('>');
106-
if (gtPos == -1 || Undefined.isUndefined(namedCaptures)) {
86+
int gtPos = replacementTemplate.indexOf('>', position + 2);
87+
if (gtPos == -1) {
10788
ref = "$<";
108-
refReplacement = ref;
89+
ops.add(new LiteralReplacement(ref));
10990
} else {
110-
ref = templateRemainder.substring(0, gtPos + 1);
111-
String groupName = templateRemainder.substring(2, gtPos);
112-
Object capture =
113-
ScriptRuntime.getObjectProp(
114-
namedCaptures, groupName, cx, scope);
115-
if (Undefined.isUndefined(capture)) {
116-
refReplacement = "";
117-
} else {
118-
refReplacement = ScriptRuntime.toString(capture);
119-
}
91+
ref = replacementTemplate.substring(position, gtPos + 1);
92+
String groupName =
93+
replacementTemplate.substring(position + 2, gtPos);
94+
ops.add(new NamedCaptureReplacement(groupName));
12095
}
12196
}
12297
break;
98+
default:
99+
ops.add(new LiteralReplacement(ref));
100+
break;
123101
}
102+
position += ref.length();
103+
start = position;
104+
} else {
105+
position++;
124106
}
107+
} else {
108+
position++;
125109
}
110+
}
111+
if (start != position) {
112+
ops.add(new LiteralReplacement(replacementTemplate.substring(start, position)));
113+
}
114+
return ops;
115+
}
126116

127-
int refLength = ref.length();
128-
templateRemainder = templateRemainder.substring(refLength);
129-
result.append(refReplacement);
117+
public static <T> String getSubstitution(
118+
Context cx,
119+
Scriptable scope,
120+
String matched,
121+
String str,
122+
int position,
123+
List<T> capturesList,
124+
Object namedCaptures,
125+
List<ReplacementOperation> replacementTemplate) {
126+
if (position > str.length()) Kit.codeBug();
127+
StringBuilder result = new StringBuilder();
128+
for (var op : replacementTemplate) {
129+
result.append(
130+
op.replacement(cx, scope, matched, str, position, capturesList, namedCaptures));
130131
}
131132
return result.toString();
132133
}
@@ -149,4 +150,181 @@ private static boolean isAsciiDigit(char c) {
149150
return false;
150151
}
151152
}
153+
154+
public abstract static class ReplacementOperation {
155+
156+
abstract <T> String replacement(
157+
Context cx,
158+
Scriptable scope,
159+
String matched,
160+
String str,
161+
int position,
162+
List<T> captures,
163+
Object namedCaptures);
164+
}
165+
166+
private static class LiteralReplacement extends ReplacementOperation {
167+
168+
private final String replacement;
169+
170+
LiteralReplacement(String replacement) {
171+
this.replacement = replacement;
172+
}
173+
174+
@Override
175+
<T> String replacement(
176+
Context cx,
177+
Scriptable scope,
178+
String matched,
179+
String str,
180+
int position,
181+
List<T> captures,
182+
Object namedCaptures) {
183+
return replacement;
184+
}
185+
}
186+
187+
private static class OneDigitCaptureReplacement extends ReplacementOperation {
188+
private final int capture;
189+
190+
OneDigitCaptureReplacement(int capture) {
191+
this.capture = capture;
192+
}
193+
194+
@Override
195+
<T> String replacement(
196+
Context cx,
197+
Scriptable scope,
198+
String matched,
199+
String str,
200+
int position,
201+
List<T> captures,
202+
Object namedCaptures) {
203+
if (capture >= 1 && capture <= captures.size()) {
204+
var v = captures.get(capture - 1);
205+
if (v == null || v == Undefined.instance) {
206+
return "";
207+
} else {
208+
return v.toString();
209+
}
210+
} else {
211+
return "$" + Integer.toString(capture);
212+
}
213+
}
214+
}
215+
216+
private static class TwoDigitCaptureReplacement extends ReplacementOperation {
217+
private final int capture;
218+
219+
TwoDigitCaptureReplacement(int capture) {
220+
this.capture = capture;
221+
}
222+
223+
@Override
224+
<T> String replacement(
225+
Context cx,
226+
Scriptable scope,
227+
String matched,
228+
String str,
229+
int position,
230+
List<T> captures,
231+
Object namedCaptures) {
232+
int i = capture;
233+
if (i > 9 && i > captures.size() && i / 10 <= captures.size()) {
234+
i = i / 10; // Just take the first digit.
235+
var v = captures.get(i - 1);
236+
if (v == null || v == Undefined.instance) {
237+
return "" + Integer.toString(capture % 10);
238+
} else {
239+
return v.toString() + Integer.toString(capture % 10);
240+
}
241+
} else if (i >= 1 && i <= captures.size()) {
242+
var v = captures.get(i - 1);
243+
if (v == null || v == Undefined.instance) {
244+
return "";
245+
} else {
246+
return v.toString();
247+
}
248+
} else {
249+
return (capture >= 10 ? "$" : "$0") + Integer.toString(capture);
250+
}
251+
}
252+
}
253+
254+
private static class FromStartToMatchReplacement extends ReplacementOperation {
255+
@Override
256+
<T> String replacement(
257+
Context cx,
258+
Scriptable scope,
259+
String matched,
260+
String str,
261+
int position,
262+
List<T> captures,
263+
Object namedCaptures) {
264+
return str.substring(0, position);
265+
}
266+
}
267+
268+
private static class MatchedReplacement extends ReplacementOperation {
269+
@Override
270+
<T> String replacement(
271+
Context cx,
272+
Scriptable scope,
273+
String matched,
274+
String str,
275+
int position,
276+
List<T> captures,
277+
Object namedCaptures) {
278+
return matched;
279+
}
280+
}
281+
282+
private static class FromMatchToEndReplacement extends ReplacementOperation {
283+
@Override
284+
<T> String replacement(
285+
Context cx,
286+
Scriptable scope,
287+
String matched,
288+
String str,
289+
int position,
290+
List<T> captures,
291+
Object namedCaptures) {
292+
int matchLength = matched.length();
293+
int tailPos = position + matchLength;
294+
return str.substring(Math.min(str.length(), tailPos));
295+
}
296+
}
297+
298+
private static class NamedCaptureReplacement extends ReplacementOperation {
299+
final String groupName;
300+
301+
NamedCaptureReplacement(String groupName) {
302+
this.groupName = groupName;
303+
}
304+
305+
@Override
306+
<T> String replacement(
307+
Context cx,
308+
Scriptable scope,
309+
String matched,
310+
String str,
311+
int position,
312+
List<T> captures,
313+
Object namedCaptures) {
314+
if (Undefined.isUndefined(namedCaptures)) {
315+
List<ReplacementOperation> ops = buildReplacementList(groupName);
316+
return "$<"
317+
+ getSubstitution(
318+
cx, scope, matched, str, position, captures, namedCaptures, ops)
319+
+ ">";
320+
}
321+
322+
Object capture = ScriptRuntime.getObjectProp(namedCaptures, groupName, cx, scope);
323+
if (Undefined.isUndefined(capture)) {
324+
return "";
325+
} else {
326+
return ScriptRuntime.toString(capture);
327+
}
328+
}
329+
}
152330
}

0 commit comments

Comments
 (0)