11package 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 */
47public 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