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