• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/*
2 * Copyright (c) 2021-2024 Huawei Device Co., Ltd.
3 * Licensed under the Apache License, Version 2.0 (the "License");
4 * you may not use this file except in compliance with the License.
5 * You may obtain a copy of the License at
6 *
7 * http://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software
10 * distributed under the License is distributed on an "AS IS" BASIS,
11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 * See the License for the specific language governing permissions and
13 * limitations under the License.
14 */
15
16package escompat
17
18/**
19 * Regular expression result descriptor
20 */
21export class RegExpExecArray /*extends Array<String>*/ {
22    /** true if match present */
23    readonly isCorrect: boolean
24    /** The 0-based index of the match in the string */
25    readonly index: number
26    /** The original string that was matched against */
27    readonly input: String
28    /** result itself */
29    readonly result: String[]
30
31    // NOTE(shumilov-petr): Make nullable when #14813 will be fixed
32    readonly indices: number[][]
33
34    // NOTE(kirill-mitkin): add groups fields
35
36    /**
37     * Returns result string by index
38     *
39     * @param index
40     *
41     * @returns resulting string
42     */
43    public get(index: int): String {
44        return this.result[index]
45    }
46
47    /**
48     * Returns result string by index
49     *
50     * @param index
51     *
52     * @returns resulting string
53     */
54    public get(index: number): String {
55        return this.get(index as int);
56    }
57
58    get length(): int {
59        return this.result.length;
60    }
61
62    constructor(index: int, input: String, result: String[], indices: number[][]) {
63        this.isCorrect = true
64        this.index = index
65        this.input = input
66        this.result = result
67        this.indices = indices
68    }
69
70    constructor(index: number, input: String, result: String[]) {
71        this.isCorrect = true
72        this.index = index
73        this.input = input
74        this.result = result
75        this.indices = []
76    }
77
78    constructor(index: number, input: String, result: String[], indices: number[][]) {
79        this(index as int, input, result, indices)
80    }
81}
82
83export type RegExpMatchArray = RegExpExecArray;
84
85class GlobalRegExpStringIterator implements IterableIterator<RegExpMatchArray> {
86    private regexp: RegExp
87    private string: String
88
89    constructor(regexp: RegExp, string: String) {
90        this.regexp = regexp
91        this.string = string
92    }
93
94    override next(): IteratorResult<RegExpMatchArray> {
95        let match = this.regexp.exec(this.string)
96        if (match == null) {
97            return new IteratorResult<RegExpMatchArray>()
98        }
99        let matchStr = match.get(0)
100        if (matchStr == "") {
101            let nextIndex = RegExp.advanceStringIndex(this.string, this.regexp.lastIndex, this.regexp.unicode)
102            this.regexp.lastIndex = nextIndex
103        }
104        return new IteratorResult<RegExpMatchArray>(match)
105    }
106
107    override $_iterator(): IterableIterator<RegExpMatchArray> {
108        return this
109    }
110}
111
112class RegExpStringIterator implements IterableIterator<RegExpMatchArray> {
113    private regexp: RegExp
114    private string: String
115    private returned: boolean
116
117    constructor(regexp: RegExp, string: String) {
118        this.regexp = regexp
119        this.string = string
120        this.returned = false
121    }
122
123    override next(): IteratorResult<RegExpMatchArray> {
124        let match = this.regexp.exec(this.string)
125        if (match == null || this.returned) {
126            return new IteratorResult<RegExpMatchArray>()
127        }
128        this.returned = true
129        return new IteratorResult<RegExpMatchArray>(match)
130    }
131
132    override $_iterator(): IterableIterator<RegExpMatchArray> {
133        return this
134    }
135}
136
137/**
138 * Regular expression
139 */
140export class RegExp extends Object {
141    private pattern_: String
142    public lastIndex: number
143    private source_: String
144    private flags_: String
145
146    private static input_: String
147    private static lastMatch_: String
148    private static lastParen_: String
149    private static leftContext_: String
150    private static rightContext_: String
151    private static $1_: String, $2_: String, $3_: String, $4_: String, $5_: String, $6_: String, $7_: String, $8_: String, $9_: String
152
153    private groupNames: String[]
154    private buffer: int[]
155
156    // NOTE(shumilov-petr): Removing the following setter leads to unspecified behaviour of es2panda
157    // The negative test '02.lexical_elements/07.keywords/types_n_12.sts' gets failed if remove this setter
158    set __removeMe(a: int) {}
159
160    /** Flags */
161    get flags(): String {
162        return this.flags_
163    }
164
165    /**
166     * Has the value true if the regular expression should be tested against
167     * all possible matches in a string
168     */
169    get global(): boolean {
170        return this.flags_.contains("g", 0)
171    }
172
173    /**
174     * Has the value true if the dot special character (.) should additionally match
175     * the line terminator characters in a string
176     */
177    get dotAll(): boolean {
178        return this.flags_.contains("s", 0)
179    }
180
181    /**
182     * Has the value true if the result of a regular expression match should contain
183     * the start and end indices of the substrings of each capture group
184     */
185    get hasIndices(): boolean {
186        return this.flags_.contains("d", 0)
187    }
188
189    /**
190     * Has the value true if case should be ignored while attempting a match in a string
191     */
192    get ignoreCase(): boolean {
193        return this.flags_.contains("i", 0)
194    }
195
196    /**
197     * Has the value true if a multiline input string should be treated as multiple lines
198     */
199    get multiline(): boolean {
200        return this.flags_.contains("m", 0)
201    }
202
203    /**
204     * Has the value true if the regex attempts to match the target string only
205     * from the index indicated by the lastIndex property
206     */
207    get sticky(): boolean {
208        return this.flags_.contains("y", 0)
209    }
210
211    /**
212     * Has the value true if 'u' flag is used
213     */
214    get unicode(): boolean {
215        return this.flags_.contains("u", 0)
216    }
217
218    /**
219     * Has the value true if 'v' flag is used
220     */
221    get unicodeSets(): boolean {
222        return this.flags_.contains("v", 0)
223    }
224
225    /**
226     * Returns a string containing the source text of this regular expression
227     */
228    get source(): String {
229        return this.source_
230    }
231
232    /**
233     * Returns the string against which a regular expression is matched
234     */
235    static get input(): String {
236        return RegExp.input_
237    }
238
239    /**
240     * Returns the string against which a regular expression is matched
241     */
242    static get $_(): String {
243        return RegExp.input_
244    }
245
246    /**
247     * Returns the last matched substring
248     */
249    static get lastMatch(): String {
250        return RegExp.lastMatch_
251    }
252
253    /**
254     * An alias for lastMatch
255     */
256    // static get $&(): String {
257    //     return RegExp.lastMatch_
258    // }
259
260    /**
261     * Returns the last parenthesized substring match, if any
262     */
263    static get lastParen(): String {
264        return RegExp.lastParen_
265    }
266
267    /**
268     * An alias for lastMatch
269     */
270    // static get $+(): String {
271    //     return RegExp.lastParen_
272    // }
273
274    /**
275     * Returns the substring preceding the most recent match
276     */
277    static get leftContext(): String {
278        return RegExp.leftContext_
279    }
280
281    /**
282     * An alias for leftContext
283     */
284    // static get $`(): String {
285    //     return RegExp.leftContext_
286    // }
287
288    /**
289     * Returns the substring following the most recent match
290     */
291    static get rightContext(): String {
292        return RegExp.rightContext_
293    }
294
295    /**
296     * An alias for rightContext
297     */
298    // static get $'(): String {
299    //     return RegExp.rightContext_
300    // }
301
302    /**
303     * Static accessor properties return parenthesized substring matches
304     */
305    static get $1(): String {
306        return RegExp.$1_
307    }
308
309    static get $2(): String {
310        return RegExp.$2_
311    }
312
313    static get $3(): String {
314        return RegExp.$3_
315    }
316
317    static get $4(): String {
318        return RegExp.$4_
319    }
320
321    static get $5(): String {
322        return RegExp.$5_
323    }
324
325    static get $6(): String {
326        return RegExp.$6_
327    }
328
329    static get $7(): String {
330        return RegExp.$7_
331    }
332
333    static get $8(): String {
334        return RegExp.$8_
335    }
336
337    static get $9(): String {
338        return RegExp.$9_
339    }
340
341    /**
342     * Constructs a new RegExp using pattern and flags
343     *
344     * @param pattern description of a pattern
345     *
346     * @param flags description of flags to be used
347     */
348    public constructor(pattern: String, flags?: String) {
349        this.init(pattern, flags)
350    }
351
352    /**
353     * Constructs a new RegExp by other RegExp. It uses other RegExp's flags if flags aren't provided
354     *
355     * @param regexp other regexp
356     *
357     * @param flags description of flags to be used
358     */
359    public constructor(regexp: RegExp, flags?: String) {
360        this.init(regexp.pattern_, __runtimeIsSameReference(flags, undefined) ? regexp.flags : flags!)
361    }
362
363    /**
364     * Constructs a new RegExp by RegExp or string
365     *
366     * @param regexp other regexp
367     *
368     * @param flags description of flags to be used
369     */
370    public constructor(regexp: RegExp | String, flags?: String) {
371        if (regexp instanceof String) {
372            this.init(regexp as String, flags)
373        } else {
374            this.init(regexp as RegExp, flags)
375        }
376    }
377
378    /**
379     * Compiles a regular expression
380     */
381    public native compile(): RegExp;
382
383    /**
384     * Recompiles a regular expression with new source and flags after the RegExp object has already been created
385     *
386     * @param pattern the text of the regular expression
387     *
388     * @param flags any combination of flag values
389     */
390    public compile(pattern: String, flags?: String): RegExp {
391        this.pattern_ = pattern
392        this.flags_ = __runtimeIsSameReference(flags, undefined) ? "" : flags!
393        this.source_ = RegExp.escapePattern(pattern)
394        return this.compile()
395    }
396
397    private native execImpl(str: String): RegExpExecArray;
398    /**
399     * Executes a search for a match in a specified string and returns a result array
400     *
401     * @param str the string against which to match the regular expression
402     *
403     * @returns RegExp result
404     *
405     * @see RegExpExecArray
406     */
407    public exec(str: String): RegExpExecArray | null {
408        const res = this.execImpl(str)
409        if (res.isCorrect) {
410            return res
411        } else {
412            return null
413        }
414    }
415
416    public search(str: String): number {
417        let previousLastIndex = this.lastIndex
418        this.lastIndex = 0
419        let result = this.exec(str)
420        this.lastIndex = previousLastIndex
421        if (result == null) {
422            return -1
423        } else {
424            return result.index
425        }
426    }
427
428    public replace(str: String, replaceValue: String): String {
429        if (this.global) {
430            this.lastIndex = 0
431        }
432        let results = new Array<RegExpExecArray>()
433        let done = false
434        while (!done) {
435            let result = this.exec(str)
436            if (result == null) {
437                break;
438            }
439            results.push(result)
440            if (!this.global) {
441                break;
442            }
443            let matchStr = result.get(0)
444            if (matchStr == "") {
445                let thisIndex = this.lastIndex
446                let nextIndex = RegExp.advanceStringIndex(str, thisIndex, this.unicode)
447                this.lastIndex = nextIndex
448            }
449        }
450        let lengthS = str.getLength()
451        let accumulatedResult = ""
452        let nextSourcePosition = 0
453        for (let i = 0; i < results.length; ++i) {
454            let result = results.at(i)!;
455            let nCaptures = result.length;
456            let matched = result.get(0);
457            let matchLength = matched.getLength();
458            let position = max(min(result.index, lengthS), 0) as int;
459            let captures = new NullableStringArray(nCaptures);
460            for (let n = 1; n < nCaptures; n++) {
461                let capN = result.get(n);
462                captures.pushBack(capN)
463            }
464            let namedCaptures: Object | undefined = /*result.groups*/ undefined;
465            let replacement = String.getSubstitution(matched, str, position as int, captures.toArray(), namedCaptures, replaceValue)
466            if (position >= nextSourcePosition) {
467                accumulatedResult += str.substring(nextSourcePosition, position) + replacement
468                nextSourcePosition = position + matchLength
469            }
470        }
471        if (nextSourcePosition >= lengthS) {
472            return accumulatedResult
473        }
474        return accumulatedResult + str.substring(nextSourcePosition)
475    }
476
477    public replace(str: String, replacer: (substr: String, args: Object[]) => String): String {
478        if (this.global) {
479            this.lastIndex = 0
480        }
481        let results = new Array<RegExpExecArray>()
482        let done = false
483        while (!done) {
484            let result = this.exec(str)
485            if (result == null) {
486                break;
487            }
488            results.push(result)
489            if (!this.global) {
490                break;
491            }
492            let matchStr = result.get(0)
493            if (matchStr == "") {
494                let thisIndex = this.lastIndex
495                let nextIndex = RegExp.advanceStringIndex(str, thisIndex, this.unicode)
496                this.lastIndex = nextIndex
497            }
498        }
499        let lengthS = str.getLength()
500        let accumulatedResult = ""
501        let nextSourcePosition = 0
502        for (let i = 0; i < results.length; ++i) {
503            let result = results.at(i)!;
504            let nCaptures = result.length;
505            let matched = result.get(0);
506            let matchLength = matched.getLength();
507            let position = max(min(result.index, lengthS), 0) as int;
508            let args = new NullableObjectArray(nCaptures);
509            for (let n = 1; n < nCaptures; n++) {
510                let capN = result.get(n);
511                args.pushBack(capN)
512            }
513            let namedCaptures: NullishType = /*result.groups*/ undefined;
514            args.pushBack(new Double(position as double));
515            args.pushBack(str)
516            if (namedCaptures != undefined) {
517                args.pushBack(namedCaptures)
518            }
519            let replacement = replacer(matched, args.toArray());
520            if (position >= nextSourcePosition) {
521                accumulatedResult += str.substring(nextSourcePosition, position) + replacement
522                nextSourcePosition = position + matchLength
523            }
524        }
525        if (nextSourcePosition >= lengthS) {
526            return accumulatedResult
527        }
528        return accumulatedResult + str.substring(nextSourcePosition)
529    }
530
531    public split(str: String, limit: Number | undefined): String[] {
532
533        let unicodeMatching = this.unicode
534        let newFlags = this.sticky ? this.flags : this.flags + "y";
535        let splitter = new RegExp(this.pattern_, newFlags)
536        let lim: long;
537        if (limit == undefined) {
538            lim = (1 << 32) -1
539        } else if (limit == 0) {
540            return new String[0]
541        } else {
542            lim = limit.unboxed() as long
543        }
544        let size = str.getLength()
545        if (size == 0) {
546            let z = splitter.exec(str)
547            if (z != null) {
548                return new String[0]
549            }
550            return [str]
551        }
552        let splittedStrings = new NullableStringArray()
553        let lastStart = 0
554        for (let lastEnd = 0; lastEnd < size;) {
555            splitter.lastIndex = lastEnd
556            let z = splitter.exec(str)
557            if (z == null) {
558                lastEnd = RegExp.advanceStringIndex(str, lastEnd, unicodeMatching)
559            } else {
560                let separatorRight = (splitter.lastIndex < size) ? splitter.lastIndex as int : size
561                if (separatorRight == lastStart) {
562                    lastEnd = RegExp.advanceStringIndex(str, lastEnd, unicodeMatching)
563                } else {
564                    let substr = str.substring(lastStart, lastEnd)
565                    splittedStrings.pushBack(substr)
566                    if (splittedStrings.size() == lim) {
567                        return splittedStrings.toArray() as String[]
568                    }
569                    lastStart = separatorRight
570                    let numberOfCaptures = z.length;
571                    for (let i = 1; i < numberOfCaptures; ++i) {
572                        let nextCapture = z.get(i);
573                        splittedStrings.pushBack(nextCapture)
574                        if (splittedStrings.size() == lim) {
575                            return splittedStrings.toArray() as String[]
576                        }
577                    }
578                    lastEnd = lastStart
579                }
580            }
581        }
582        let substr = str.substring(lastStart, size)
583        splittedStrings.pushBack(substr)
584        return splittedStrings.toArray() as String[]
585    }
586
587    public match(str: String): RegExpMatchArray | null {
588        if (!this.global) {
589            return this.exec(str)
590        }
591        this.lastIndex = 0;
592        let matches = new NullableStringArray();
593        let n = 0
594        while (true) {
595            let result = this.exec(str)
596            if (result == null) {
597                if (n == 0) {
598                    return null
599                }
600                return new RegExpMatchArray(-1, "", matches.toArray() as String[])
601            }
602            else {
603                let matchStr = result.get(0)
604                matches.pushBack(matchStr)
605                if (matchStr == "") {
606                    this.lastIndex = RegExp.advanceStringIndex(str, this.lastIndex, this.unicode)
607                }
608                n++
609            }
610        }
611    }
612
613    public matchAll(str: String): IterableIterator<RegExpMatchArray> {
614        let matcher = new RegExp(this)
615        matcher.lastIndex = this.lastIndex
616        if (matcher.global) {
617            return new GlobalRegExpStringIterator(matcher, str)
618        } else {
619            return new RegExpStringIterator(matcher, str)
620        }
621    }
622
623    /**
624     * Executes a search for a match between a regular expression and specified string
625     *
626     * @param str the string against which to match the regular expression
627     *
628     * @returns true if match was found
629     */
630    public test(str: String): boolean {
631        return this.execImpl(str).isCorrect
632    }
633
634    /**
635     * Returns a string representing the given object
636     *
637     * @returns a string representing the given object
638     */
639    public override toString(): String {
640        return "/" + this.source + "/" + this.flags_
641    }
642
643    /**
644     * Returns next index from a passed one
645     *
646     * @param s
647     *
648     * @param index start position
649     *
650     * @param unicode true if unicode is used
651     *
652     * @returns new index
653     */
654    public static advanceStringIndex(s: String, index: int, unicode: boolean): int {
655        if (!unicode) {
656            return index + 1
657        }
658        let length = s.getLength();
659        if (index + 1 >= length) {
660            return index + 1
661        }
662        return index + s.codePointCount(index, index + 1);
663    }
664
665    /**
666     * Returns next index from a passed one
667     *
668     * @param s
669     *
670     * @param index start position
671     *
672     * @param unicode true if unicode is used
673     *
674     * @returns new index
675     */
676    public static advanceStringIndex(s: String, index: number, unicode: boolean): number {
677        return RegExp.advanceStringIndex(s, index as int, unicode)
678    }
679
680    private static escapePattern(pattern: String): String {
681        if (pattern == "") {
682            return "(?:)"
683        }
684        let s = pattern.replaceAll("/", "\\/")
685        return s.replaceAll("\\", "\\")
686    }
687
688    /**
689     * Inits regexp from pattern and flags
690     *
691     * @param pattern description of a pattern
692     *
693     * @param flags description of flags to be used
694     */
695    private init(pattern: String, flags?: String) {
696        this.pattern_ = pattern
697        this.flags_ = __runtimeIsSameReference(flags, undefined) ? "" : flags!
698        this.source_ = RegExp.escapePattern(pattern)
699        this.compile()
700    }
701
702    /**
703     * Inits regexp from RegExp. It uses regexp's flags if flags aren't provided
704     *
705     * @param regexp other regexp
706     *
707     * @param flags description of flags to be used
708     */
709    private init(regexp: RegExp, flags?: String) {
710        this.init(regexp.pattern_, __runtimeIsSameReference(flags, undefined) ? regexp.flags : flags!)
711    }
712}
713