]> git.kaiwu.me - njs.git/commitdiff
Fixed String.prototype.replace() according to the specification.
authorDmitry Volyntsev <xeioex@nginx.com>
Thu, 2 Jul 2020 14:00:16 +0000 (14:00 +0000)
committerDmitry Volyntsev <xeioex@nginx.com>
Thu, 2 Jul 2020 14:00:16 +0000 (14:00 +0000)
This closes #308 issue on Github.

src/njs_regexp.c
src/njs_string.c
src/njs_string.h
src/njs_value.c
src/njs_value.h
src/test/njs_unit_test.c

index fbed0fd52100c7eb647d2af0ca657ae6dc49794d..374610e45c15900b7a088c75f6764653af3f5c06 100644 (file)
@@ -1161,6 +1161,314 @@ njs_regexp_string_create(njs_vm_t *vm, njs_value_t *value, u_char *start,
 }
 
 
+static njs_int_t
+njs_regexp_prototype_symbol_replace(njs_vm_t *vm, njs_value_t *args,
+    njs_uint_t nargs, njs_index_t unused)
+{
+    u_char             *p;
+    int64_t            n, last_index, ncaptures, pos, next_pos, size, length;
+    njs_str_t          rep, m;
+    njs_int_t          ret;
+    njs_arr_t          results;
+    njs_chb_t          chain;
+    njs_uint_t         i;
+    njs_bool_t         global;
+    njs_array_t        *array;
+    njs_value_t        *arguments, *r, *rx, *string, *replace;
+    njs_value_t        s_lvalue, r_lvalue, value, matched, groups, retval;
+    njs_function_t     *func_replace;
+    njs_string_prop_t  s;
+
+    static const njs_value_t  string_global = njs_string("global");
+    static const njs_value_t  string_groups = njs_string("groups");
+    static const njs_value_t  string_index = njs_string("index");
+    static const njs_value_t  string_lindex = njs_string("lastIndex");
+
+    rx = njs_argument(args, 0);
+
+    if (njs_slow_path(!njs_is_object(rx))) {
+        njs_type_error(vm, "\"this\" is not object");
+        return NJS_ERROR;
+    }
+
+    string = njs_lvalue_arg(&s_lvalue, args, nargs, 1);
+
+    ret = njs_value_to_string(vm, string, string);
+    if (njs_slow_path(ret != NJS_OK)) {
+        return ret;
+    }
+
+    length = njs_string_prop(&s, string);
+
+    rep.start = NULL;
+    rep.length = 0;
+
+    replace = njs_lvalue_arg(&r_lvalue, args, nargs, 2);
+    func_replace = njs_is_function(replace) ? njs_function(replace) : NULL;
+
+    if (!func_replace) {
+        ret = njs_value_to_string(vm, replace, replace);
+        if (njs_slow_path(ret != NJS_OK)) {
+            return ret;
+        }
+    }
+
+    ret = njs_value_property(vm, rx, njs_value_arg(&string_global), &value);
+    if (njs_slow_path(ret == NJS_ERROR)) {
+        return NJS_ERROR;
+    }
+
+    global = njs_bool(&value);
+
+    if (global) {
+        njs_set_number(&value, 0);
+        ret = njs_value_property_set(vm, rx, njs_value_arg(&string_lindex),
+                                     &value);
+        if (njs_slow_path(ret != NJS_OK)) {
+            return NJS_ERROR;
+        }
+    }
+
+    njs_chb_init(&chain, vm->mem_pool);
+
+    results.separate = 0;
+    results.pointer = 0;
+
+    r = njs_arr_init(vm->mem_pool, &results, NULL, 4, sizeof(njs_value_t));
+    if (njs_slow_path(r == NULL)) {
+        return NJS_ERROR;
+    }
+
+    for ( ;; ) {
+        r = njs_arr_add(&results);
+        if (njs_slow_path(r == NULL)) {
+            ret = NJS_ERROR;
+            goto exception;
+        }
+
+        ret = njs_regexp_exec(vm, rx, string, r);
+        if (njs_slow_path(ret != NJS_OK)) {
+            goto exception;
+        }
+
+        if (njs_is_null(r) || !global) {
+            break;
+        }
+
+        if (njs_fast_path(njs_is_fast_array(r) && njs_array_len(r) != 0)) {
+            value = njs_array_start(r)[0];
+
+        } else {
+            ret = njs_value_property_i64(vm, r, 0, &value);
+            if (njs_slow_path(ret == NJS_ERROR)) {
+                goto exception;
+            }
+        }
+
+        ret = njs_value_to_string(vm, &value, &value);
+        if (njs_slow_path(ret != NJS_OK)) {
+            goto exception;
+        }
+
+        if (njs_string_length(&value) != 0) {
+            continue;
+        }
+
+        ret = njs_value_property(vm, rx, njs_value_arg(&string_lindex), &value);
+        if (njs_slow_path(ret == NJS_ERROR)) {
+            goto exception;
+        }
+
+        ret = njs_value_to_length(vm, &value, &last_index);
+        if (njs_slow_path(ret != NJS_OK)) {
+            goto exception;
+        }
+
+        njs_set_number(&value, last_index + 1);
+        ret = njs_value_property_set(vm, rx, njs_value_arg(&string_lindex),
+                                     &value);
+        if (njs_slow_path(ret != NJS_OK)) {
+            goto exception;
+        }
+    }
+
+    i = 0;
+    next_pos = 0;
+
+    while (i < results.items) {
+        r = njs_arr_item(&results, i++);
+
+        if (njs_slow_path(njs_is_null(r))) {
+            break;
+        }
+
+        ret = njs_value_property_i64(vm, r, 0, &matched);
+        if (njs_slow_path(ret == NJS_ERROR)) {
+            goto exception;
+        }
+
+        ret = njs_value_to_string(vm, &matched, &matched);
+        if (njs_slow_path(ret != NJS_OK)) {
+            goto exception;
+        }
+
+        ret = njs_value_property(vm, r, njs_value_arg(&string_index), &value);
+        if (njs_slow_path(ret == NJS_ERROR)) {
+            goto exception;
+        }
+
+        ret = njs_value_to_integer(vm, &value, &pos);
+        if (njs_slow_path(ret != NJS_OK)) {
+            goto exception;
+        }
+
+        if ((size_t) length != s.size) {
+            /* UTF-8 string. */
+            pos = njs_string_offset(s.start, s.start + s.size, pos) - s.start;
+        }
+
+        pos = njs_max(njs_min(pos, (int64_t) s.size), 0);
+
+        if (njs_fast_path(njs_is_fast_array(r))) {
+            array = njs_array(r);
+
+            arguments = array->start;
+            ncaptures = array->length;
+
+            for (n = 1; n < ncaptures; n++) {
+                if (njs_is_undefined(&arguments[n])) {
+                    continue;
+                }
+
+                ret = njs_value_to_string(vm, &arguments[n], &arguments[n]);
+                if (njs_slow_path(ret == NJS_ERROR)) {
+                    goto exception;
+                }
+            }
+
+        } else {
+            ret = njs_object_length(vm, r, &ncaptures);
+            if (njs_slow_path(ret != NJS_OK)) {
+                goto exception;
+            }
+
+            array = njs_array_alloc(vm, 0, ncaptures, 0);
+            if (njs_slow_path(array == NULL)) {
+                goto exception;
+            }
+
+            arguments = array->start;
+            arguments[0] = matched;
+
+            for (n = 1; n < ncaptures; n++) {
+                ret = njs_value_property_i64(vm, r, n, &arguments[n]);
+                if (njs_slow_path(ret == NJS_ERROR)) {
+                    goto exception;
+                }
+
+                if (njs_is_undefined(&arguments[n])) {
+                    continue;
+                }
+
+                ret = njs_value_to_string(vm, &arguments[n], &arguments[n]);
+                if (njs_slow_path(ret == NJS_ERROR)) {
+                    goto exception;
+                }
+            }
+        }
+
+        ret = njs_value_property(vm, r, njs_value_arg(&string_groups), &groups);
+        if (njs_slow_path(ret == NJS_ERROR)) {
+            goto exception;
+        }
+
+        if (!func_replace) {
+            if (njs_is_defined(&groups)) {
+                ret = njs_value_to_object(vm, &groups);
+                if (njs_slow_path(ret != NJS_OK)) {
+                    return ret;
+                }
+            }
+
+            ret = njs_string_get_substitution(vm, &matched, string, pos,
+                                              arguments, ncaptures - 1, &groups,
+                                              replace, &retval);
+
+        } else {
+            ret = njs_array_expand(vm, array, 0,
+                                   njs_is_defined(&groups) ? 3 : 2);
+            if (njs_slow_path(ret != NJS_OK)) {
+                goto exception;
+            }
+
+            arguments = array->start;
+            njs_set_number(&arguments[n++], pos);
+            arguments[n++] = *string;
+
+            if (njs_is_defined(&groups)) {
+                arguments[n++] = groups;
+            }
+
+            ret = njs_function_call(vm, func_replace,
+                                    njs_value_arg(&njs_value_undefined),
+                                    arguments, n, &retval);
+        }
+
+        if (njs_slow_path(ret == NJS_ERROR)) {
+            return NJS_ERROR;
+        }
+
+        ret = njs_value_to_string(vm, &retval, &retval);
+        if (njs_slow_path(ret != NJS_OK)) {
+            goto exception;
+        }
+
+        if (pos >= next_pos) {
+            njs_chb_append(&chain, &s.start[next_pos], pos - next_pos);
+
+            njs_string_get(&retval, &rep);
+            njs_chb_append_str(&chain, &rep);
+
+            njs_string_get(&matched, &m);
+
+            next_pos = pos + (int64_t) m.length;
+        }
+    }
+
+    if (next_pos < (int64_t) s.size) {
+        njs_chb_append(&chain, &s.start[next_pos], s.size - next_pos);
+    }
+
+    size = njs_chb_size(&chain);
+    if (njs_slow_path(size < 0)) {
+        njs_memory_error(vm);
+        ret = NJS_ERROR;
+        goto exception;
+    }
+
+    length = njs_chb_utf8_length(&chain);
+
+    p = njs_string_alloc(vm, &vm->retval, size, length);
+    if (njs_slow_path(p == NULL)) {
+        ret = NJS_ERROR;
+        goto exception;
+    }
+
+    njs_chb_join_to(&chain, p);
+
+    ret = NJS_OK;
+
+exception:
+
+    njs_chb_destroy(&chain);
+    njs_arr_destroy(&results);
+
+    return ret;
+}
+
+
+
+
 static const njs_object_prop_t  njs_regexp_constructor_properties[] =
 {
     {
@@ -1252,6 +1560,14 @@ static const njs_object_prop_t  njs_regexp_prototype_properties[] =
         .writable = 1,
         .configurable = 1,
     },
+
+    {
+        .type = NJS_PROPERTY,
+        .name = njs_wellknown_symbol(NJS_SYMBOL_REPLACE),
+        .value = njs_native_function(njs_regexp_prototype_symbol_replace, 2),
+        .writable = 1,
+        .configurable = 1,
+    },
 };
 
 
index ba7e58694a2e996fb1906ca45efd15c25a680fcf..482bfb4e12336bbb85f819d37641adf64d8f3df3 100644 (file)
 #define NJS_TRIM_END    2
 
 
-typedef struct {
-    u_char                     *start;
-    size_t                     size;
-    njs_value_t                value;
-} njs_string_replace_part_t;
-
-
-#define NJS_SUBST_COPY        255
-#define NJS_SUBST_PRECEDING   254
-#define NJS_SUBST_FOLLOWING   253
-
-
-typedef struct {
-     uint32_t  type;
-     uint32_t  size;
-     u_char    *start;
-} njs_string_subst_t;
-
-
-typedef struct {
-    njs_value_t                retval;
-
-    njs_arr_t                  parts;
-    njs_string_replace_part_t  array[3];
-    njs_string_replace_part_t  *part;
-
-    njs_arr_t                  *substitutions;
-    njs_function_t             *function;
-
-    njs_regex_match_data_t     *match_data;
-
-    njs_bool_t                 empty;
-
-    njs_utf8_t                 utf8:8;
-    njs_regexp_utf8_t          type:8;
-} njs_string_replace_t;
-
-
 static void njs_encode_base64_core(njs_str_t *dst, const njs_str_t *src,
     const u_char *basis, njs_uint_t padding);
 static njs_int_t njs_decode_base64_core(njs_vm_t *vm,
@@ -72,25 +34,6 @@ static njs_int_t njs_string_match_multiple(njs_vm_t *vm, njs_value_t *args,
     njs_regexp_pattern_t *pattern);
 static njs_int_t njs_string_split_part_add(njs_vm_t *vm, njs_array_t *array,
     njs_utf8_t utf8, const u_char *start, size_t size);
-static njs_int_t njs_string_replace_regexp(njs_vm_t *vm, njs_value_t *this,
-    njs_value_t *regex, njs_string_replace_t *r);
-static njs_int_t njs_string_replace_regexp_function(njs_vm_t *vm,
-    njs_value_t *this, njs_value_t *regex, njs_string_replace_t *r,
-    int *captures, njs_uint_t n);
-static njs_int_t njs_string_replace_regexp_join(njs_vm_t *vm,
-    njs_string_replace_t *r);
-static njs_int_t njs_string_replace_search(njs_vm_t *vm, njs_value_t *this,
-    njs_value_t *search, njs_string_replace_t *r);
-static njs_int_t njs_string_replace_search_function(njs_vm_t *vm,
-        njs_value_t *this, njs_value_t *search, njs_string_replace_t *r);
-static njs_int_t njs_string_replace_parse(njs_vm_t *vm,
-    njs_string_replace_t *r, u_char *p, u_char *end, size_t size,
-    njs_uint_t ncaptures);
-static njs_int_t njs_string_replace_substitute(njs_vm_t *vm,
-    njs_string_replace_t *r, int *captures);
-static njs_int_t njs_string_replace_join(njs_vm_t *vm, njs_string_replace_t *r);
-static void njs_string_replacement_copy(njs_string_replace_part_t *string,
-    const njs_value_t *value);
 
 
 #define njs_base64_encoded_length(len)  (((len + 2) / 3) * 4)
@@ -3406,806 +3349,294 @@ njs_string_split_part_add(njs_vm_t *vm, njs_array_t *array, njs_utf8_t utf8,
 }
 
 
-static njs_int_t
-njs_string_prototype_replace(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
-    njs_index_t unused)
+njs_int_t
+njs_string_get_substitution(njs_vm_t *vm, njs_value_t *matched,
+    njs_value_t *string, int64_t pos, njs_value_t *captures, int64_t ncaptures,
+    njs_value_t *groups, njs_value_t *replacement, njs_value_t *retval)
 {
-    u_char                *p, *start, *end;
-    njs_int_t             ret;
-    njs_uint_t            ncaptures;
-    njs_value_t           *this, *search, *replace;
-    njs_value_t           search_lvalue, replace_lvalue;
-    njs_regex_t           *regex;
-    njs_string_prop_t     string;
-    njs_string_replace_t  *r, string_replace;
-
-    ret = njs_string_object_validate(vm, njs_arg(args, nargs, 0));
-    if (njs_slow_path(ret != NJS_OK)) {
-        return ret;
-    }
-
-    this = njs_argument(args, 0);
-
-    if (nargs == 1) {
-        goto original;
-    }
-
-    search = njs_lvalue_arg(&search_lvalue, args, nargs, 1);
-    replace = njs_lvalue_arg(&replace_lvalue, args, nargs, 2);
-
-    (void) njs_string_prop(&string, this);
-
-    if (string.size == 0) {
-        goto original;
-    }
-
-    r = &string_replace;
-
-    r->utf8 = NJS_STRING_BYTE;
-    r->type = NJS_REGEXP_BYTE;
-
-    if (string.length != 0) {
-        r->utf8 = NJS_STRING_ASCII;
-        r->type = NJS_REGEXP_UTF8;
-
-        if (string.length != string.size) {
-            r->utf8 = NJS_STRING_UTF8;
-        }
-    }
-
-    if (njs_is_regexp(search)) {
-        regex = &njs_regexp_pattern(search)->regex[r->type];
-
-        if (!njs_regex_is_valid(regex)) {
-            goto original;
-        }
+    int64_t      tail, size, length, n;
+    u_char       c, c2, *p, *r, *end;
+    njs_str_t    rep, m, str, cap;
+    njs_int_t    ret;
+    njs_chb_t    chain;
+    njs_value_t  name, value;
 
-        ncaptures = njs_regex_ncaptures(regex);
+    njs_string_get(replacement, &rep);
+    p = rep.start;
+    end = rep.start + rep.length;
 
-    } else {
-        regex = NULL;
-        ncaptures = 1;
+    njs_chb_init(&chain, vm->mem_pool);
 
-        if (!njs_is_string(search)) {
-            ret = njs_value_to_string(vm, search, search);
-            if (njs_slow_path(ret != NJS_OK)) {
-                return ret;
+    while (p < end) {
+        r = njs_strlchr(p, end, '$');
+        if (r == NULL || r == &end[-1]) {
+            if (njs_fast_path(p == rep.start)) {
+                *retval = *replacement;
+                return NJS_OK;
             }
-        }
-    }
-
-    /* This cannot fail. */
-    r->part = njs_arr_init(vm->mem_pool, &r->parts, &r->array,
-                           3, sizeof(njs_string_replace_part_t));
-
-    r->substitutions = NULL;
-    r->function = NULL;
-
-    /* A literal replacement is stored in the second part. */
 
-    if (nargs == 2) {
-        njs_string_replacement_copy(&r->part[1], &njs_string_undefined);
-
-    } else if (njs_is_function(replace)) {
-        r->function = njs_function(replace);
-
-    } else {
-        if (njs_slow_path(!njs_is_string(replace))) {
-            ret = njs_value_to_string(vm, replace, replace);
-            if (njs_slow_path(ret != NJS_OK)) {
-                return ret;
-            }
+            njs_chb_append(&chain, p, end - p);
+            goto done;
         }
 
-        njs_string_replacement_copy(&r->part[1], replace);
+        njs_chb_append(&chain, p, r - p);
+        p = r;
 
-        start = r->part[1].start;
+        c = r[1];
 
-        if (start == NULL) {
-            start = r->part[1].value.short_string.start;
-        }
+        switch (c) {
+        case '$':
+            njs_chb_append_literal(&chain, "$");
+            p += 2;
+            break;
 
-        end = start + r->part[1].size;
+        case '&':
+            njs_string_get(matched, &m);
+            njs_chb_append_str(&chain, &m);
+            p += 2;
+            break;
 
-        for (p = start; p < end; p++) {
-            if (*p == '$') {
-                ret = njs_string_replace_parse(vm, r, p, end, p - start,
-                                               ncaptures);
-                if (njs_slow_path(ret != NJS_OK)) {
-                    return ret;
-                }
+        case '`':
+            njs_string_get(string, &str);
+            njs_chb_append(&chain, str.start, pos);
+            p += 2;
+            break;
+
+        case '\'':
+            njs_string_get(matched, &m);
+            tail = pos + m.length;
 
-                /* Reset parts array to the subject string only. */
-                r->parts.items = 1;
+            njs_string_get(string, &str);
+            njs_chb_append(&chain, &str.start[tail],
+                           njs_max((int64_t) str.length - tail, 0));
+            p += 2;
+            break;
 
+        case '<':
+            r = njs_strlchr(p, end, '>');
+            if (r == NULL) {
+                njs_chb_append(&chain, p, 2);
+                p += 2;
                 break;
             }
-        }
-    }
 
-    r->part[0].start = string.start;
-    r->part[0].size = string.size;
-    njs_set_invalid(&r->part[0].value);
+            p += 2;
 
-    if (regex != NULL) {
-        r->match_data = njs_regex_match_data(regex, vm->regex_context);
-        if (njs_slow_path(r->match_data == NULL)) {
-            return NJS_ERROR;
-        }
-
-        return njs_string_replace_regexp(vm, this, search, r);
-    }
-
-    return njs_string_replace_search(vm, this, search, r);
-
-original:
-
-    njs_string_copy(&vm->retval, this);
-
-    return NJS_OK;
-}
-
-
-static njs_int_t
-njs_string_replace_regexp(njs_vm_t *vm, njs_value_t *this, njs_value_t *regex,
-    njs_string_replace_t *r)
-{
-    int                        *captures;
-    u_char                     *p, *start;
-    njs_int_t                  ret;
-    const u_char               *end;
-    njs_regexp_pattern_t       *pattern;
-    njs_string_replace_part_t  replace;
-
-    pattern = njs_regexp_pattern(regex);
-    end = r->part[0].start + r->part[0].size;
-
-    replace = r->part[1];
-
-    do {
-        ret = njs_regexp_match(vm, &pattern->regex[r->type],
-                               r->part[0].start, 0, r->part[0].size,
-                               r->match_data);
-
-        if (ret < 0) {
-            if (njs_slow_path(ret != NJS_REGEX_NOMATCH)) {
-                return NJS_ERROR;
+            if (groups == NULL) {
+                break;
             }
 
-            break;
-        }
-
-        captures = njs_regex_captures(r->match_data);
-
-        if (r->substitutions != NULL) {
-            ret = njs_string_replace_substitute(vm, r, captures);
+            ret = njs_vm_value_string_set(vm, &name, p, r - p);
             if (njs_slow_path(ret != NJS_OK)) {
-                return ret;
+                goto exception;
             }
 
-            if (!pattern->global) {
-                return njs_string_replace_regexp_join(vm, r);
-            }
-
-            continue;
-        }
-
-        if (r->part != r->parts.start) {
-            r->part = njs_arr_add(&r->parts);
-            if (njs_slow_path(r->part == NULL)) {
-                return NJS_ERROR;
-            }
+            p = r + 1;
 
-            r->part = njs_arr_add(&r->parts);
-            if (njs_slow_path(r->part == NULL)) {
-                return NJS_ERROR;
+            ret = njs_value_property(vm, groups, &name, &value);
+            if (njs_slow_path(ret == NJS_ERROR)) {
+                goto exception;
             }
 
-            r->part -= 2;
-        }
-
-        if (captures[1] == 0) {
-
-            /* Empty match. */
+            if (njs_is_defined(&value)) {
+                ret = njs_value_to_string(vm, &value, &value);
+                if (njs_slow_path(ret == NJS_ERROR)) {
+                    goto exception;
+                }
 
-            start = r->part[0].start;
+                njs_string_get(&value, &str);
+                njs_chb_append_str(&chain, &str);
+            }
 
-            if (start < end) {
-                p = (r->utf8 != NJS_STRING_BYTE)
-                    ? (u_char *) njs_utf8_next(start, end) : start + 1;
+            break;
 
-                r->part[1].start = start;
-                r->part[1].size = p - start;
+        default:
+            if (c >= '0' && c <= '9') {
+                n = c - '0';
 
-                r->part[2].start = p;
-                r->part[2].size = end - p;
+                c2 = (&r[2] < end) ? r[2] : 0;
 
-            } else {
-                r->part[1].size = 0;
-                r->part[2].size = 0;
+                if (c2 >= '0' && c2 <= '9'
+                    && (n * 10 + (c2 - '0')) <= ncaptures)
+                {
+                    n = n * 10 + (c2 - '0');
 
-                /* To exit the loop. */
-                r->part[2].start = start + 1;
-            }
+                } else {
+                    c2 = 0;
+                }
 
-            if (r->function != NULL) {
-                return njs_string_replace_regexp_function(vm, this, regex, r,
-                                                          captures, ret);
-            }
+                if (n == 0 || n > ncaptures) {
+                    njs_chb_append(&chain, p, (c2 != 0) ? 3 : 2);
+                    p += (c2 != 0) ? 3 : 2;
+                    break;
+                }
 
-            r->part[0] = replace;
+                p += (c2 != 0) ? 3 : 2;
 
-        } else {
-            r->part[2].start = r->part[0].start + captures[1];
-            r->part[2].size = r->part[0].size - captures[1];
-            njs_set_invalid(&r->part[2].value);
+                if (njs_is_defined(&captures[n])) {
+                    njs_string_get(&captures[n], &cap);
+                    njs_chb_append_str(&chain, &cap);
+                }
 
-            if (r->function != NULL) {
-                return njs_string_replace_regexp_function(vm, this, regex, r,
-                                                          captures, ret);
+                break;
             }
 
-            r->part[0].size = captures[0];
-
-            r->part[1] = replace;
-        }
-
-        if (!pattern->global) {
-            return njs_string_replace_regexp_join(vm, r);
+            njs_chb_append_literal(&chain, "$");
+            p += 1;
+            break;
         }
-
-        r->part += 2;
-
-    } while (r->part[0].start <= end);
-
-    if (r->part != r->parts.start) {
-        return njs_string_replace_regexp_join(vm, r);
     }
 
-    njs_regex_match_data_free(r->match_data, vm->regex_context);
-
-    njs_arr_destroy(&r->parts);
-
-    njs_string_copy(&vm->retval, this);
-
-    return NJS_OK;
-}
-
-
-static njs_int_t
-njs_string_replace_regexp_function(njs_vm_t *vm, njs_value_t *this,
-    njs_value_t *regex, njs_string_replace_t *r, int *captures, njs_uint_t n)
-{
-    u_char             *start;
-    size_t             size, length;
-    njs_int_t          ret;
-    njs_uint_t         i, k;
-    njs_value_t        *arguments;
-    njs_string_prop_t  string;
-
-    if (njs_slow_path((n + 3) >= UINT32_MAX / sizeof(njs_value_t))) {
-        njs_memory_error(vm);
-        return NJS_ERROR;
-    }
-
-    njs_set_invalid(&r->retval);
+done:
 
-    arguments = njs_mp_alloc(vm->mem_pool, (n + 3) * sizeof(njs_value_t));
-    if (njs_slow_path(arguments == NULL)) {
+    size = njs_chb_size(&chain);
+    if (njs_slow_path(size < 0)) {
         njs_memory_error(vm);
-        return NJS_ERROR;
-    }
-
-    njs_set_undefined(&arguments[0]);
-
-    /* Matched substring and parenthesized submatch strings. */
-    for (k = 0, i = 1; i <= n; i++) {
-
-        start = r->part[0].start + captures[k];
-        size = captures[k + 1] - captures[k];
-        k += 2;
-
-        length = njs_string_calc_length(r->utf8, start, size);
-
-        ret = njs_string_new(vm, &arguments[i], start, size, length);
-        if (njs_slow_path(ret != NJS_OK)) {
-            return NJS_ERROR;
-        }
-    }
-
-    r->empty = (captures[0] == captures[1]);
-
-    /* The offset of the matched substring. */
-    njs_set_number(&arguments[n + 1], captures[0]);
-
-    /* The whole string being examined. */
-    length = njs_string_calc_length(r->utf8, r->part[0].start, r->part[0].size);
-
-    (void) njs_string_prop(&string, this);
-
-    ret = njs_string_new(vm, &arguments[n + 2], string.start, string.size,
-                         length);
-
-    if (njs_slow_path(ret != NJS_OK)) {
-        return NJS_ERROR;
+        ret = NJS_ERROR;
+        goto exception;
     }
 
-    r->part[0].size = captures[0];
+    length = njs_chb_utf8_length(&chain);
 
-    ret = njs_function_apply(vm, r->function, arguments, n + 3, &r->retval);
-    if (njs_slow_path(ret != NJS_OK)) {
+    p = njs_string_alloc(vm, retval, size, length);
+    if (njs_slow_path(p == NULL)) {
+        ret = NJS_ERROR;
         goto exception;
     }
 
-    if (njs_slow_path(!njs_is_string(&r->retval))) {
-        ret = njs_value_to_string(vm, &r->retval, &r->retval);
-        if (njs_slow_path(ret != NJS_OK)) {
-            goto exception;
-        }
-    }
-
-    njs_string_replacement_copy(&r->part[r->empty ? 0 : 1], &r->retval);
-
-    if (njs_regexp_pattern(regex)->global) {
-        r->part += 2;
+    njs_chb_join_to(&chain, p);
 
-        if (r->part[0].start > (string.start + string.size)) {
-            return njs_string_replace_regexp_join(vm, r);
-        }
-
-        return njs_string_replace_regexp(vm, this, regex, r);
-    }
-
-    return njs_string_replace_regexp_join(vm, r);
+    ret = NJS_OK;
 
 exception:
 
-    njs_regex_match_data_free(r->match_data, vm->regex_context);
-
-    return NJS_ERROR;
-}
-
-
-static njs_int_t
-njs_string_replace_regexp_join(njs_vm_t *vm, njs_string_replace_t *r)
-{
-    njs_regex_match_data_free(r->match_data, vm->regex_context);
+    njs_chb_destroy(&chain);
 
-    return njs_string_replace_join(vm, r);
+    return NJS_OK;
 }
 
 
 static njs_int_t
-njs_string_replace_search(njs_vm_t *vm, njs_value_t *this, njs_value_t *search,
-    njs_string_replace_t *r)
+njs_string_prototype_replace(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
+    njs_index_t unused)
 {
-    int        captures[2];
-    u_char     *p, *end;
-    size_t     size;
-    njs_int_t  ret;
-    njs_str_t  string;
-
-    njs_string_get(search, &string);
-
-    p = r->part[0].start;
-    end = (p + r->part[0].size) - (string.length - 1);
+    u_char             *r;
+    size_t             length, search_length, ret_length, size;
+    int64_t            pos;
+    njs_int_t          ret;
+    njs_value_t        *this, *search, *replace;
+    njs_value_t        search_lvalue, replace_lvalue, replacer, retval,
+                       arguments[3];
+    const u_char       *p;
+    njs_function_t     *func_replace;
+    njs_string_prop_t  string, s, ret_string;
 
-    while (p < end) {
-        if (memcmp(p, string.start, string.length) == 0) {
+    static const njs_value_t  replace_key =
+                                      njs_wellknown_symbol(NJS_SYMBOL_REPLACE);
 
-            if (r->substitutions != NULL) {
-                captures[0] = p - r->part[0].start;
-                captures[1] = captures[0] + string.length;
+    this = njs_argument(args, 0);
 
-                ret = njs_string_replace_substitute(vm, r, captures);
-                if (njs_slow_path(ret != NJS_OK)) {
-                    return ret;
-                }
+    if (njs_slow_path(njs_is_null_or_undefined(this))) {
+        njs_type_error(vm, "cannot convert \"%s\"to object",
+                       njs_type_string(this->type));
+        return NJS_ERROR;
+    }
 
-            } else {
-                r->part[2].start = p + string.length;
-                size = p - r->part[0].start;
-                r->part[2].size = r->part[0].size - size - string.length;
-                r->part[0].size = size;
-                njs_set_invalid(&r->part[2].value);
-
-                if (r->function != NULL) {
-                    return njs_string_replace_search_function(vm, this, search,
-                                                              r);
-                }
-            }
+    search = njs_lvalue_arg(&search_lvalue, args, nargs, 1);
+    replace = njs_lvalue_arg(&replace_lvalue, args, nargs, 2);
 
-            return njs_string_replace_join(vm, r);
+    if (!njs_is_null_or_undefined(search)) {
+        ret = njs_value_method(vm, search, njs_value_arg(&replace_key),
+                               &replacer);
+        if (njs_slow_path(ret != NJS_OK)) {
+            return ret;
         }
 
-        if (r->utf8 < 2) {
-            p++;
+        if (njs_is_defined(&replacer)) {
+            arguments[0] = *this;
+            arguments[1] = *replace;
 
-        } else {
-            p = (u_char *) njs_utf8_next(p, end);
+            return njs_function_call(vm, njs_function(&replacer), search,
+                                     arguments, 2, &vm->retval);
         }
     }
 
-    njs_string_copy(&vm->retval, this);
-
-    return NJS_OK;
-}
-
-
-static njs_int_t
-njs_string_replace_search_function(njs_vm_t *vm, njs_value_t *this,
-    njs_value_t *search, njs_string_replace_t *r)
-{
-    njs_int_t    ret;
-    njs_value_t  string;
-    njs_value_t  arguments[4];
-
-    njs_set_undefined(&arguments[0]);
-
-    /* GC, args[0], args[1] */
-
-    /* Matched substring, it is the same as the args[1]. */
-    arguments[1] = *search;
-
-    /* The offset of the matched substring. */
-    njs_set_number(&arguments[2], r->part[0].size);
-
-    /* The whole string being examined. */
-    arguments[3] = *this;
-
-    ret = njs_function_apply(vm, r->function, arguments, 4, &r->retval);
-
+    ret = njs_value_to_string(vm, this, this);
     if (njs_slow_path(ret != NJS_OK)) {
         return ret;
     }
 
-    if (!njs_is_primitive(&r->retval)) {
-        ret = njs_value_to_string(vm, &r->retval, &r->retval);
-        if (ret != NJS_OK) {
-            return ret;
-        }
-    }
-
-    ret = njs_primitive_value_to_string(vm, &string, &r->retval);
+    ret = njs_value_to_string(vm, search, search);
     if (njs_slow_path(ret != NJS_OK)) {
-        njs_type_error(vm, "cannot convert primitive value to string: %s",
-                       njs_type_string(r->retval.type));
-
-        return NJS_ERROR;
+        return ret;
     }
 
-    njs_string_replacement_copy(&r->part[1], &string);
-
-    return njs_string_replace_join(vm, r);
-}
-
+    func_replace = njs_is_function(replace) ? njs_function(replace) : NULL;
 
-static njs_int_t
-njs_string_replace_parse(njs_vm_t *vm, njs_string_replace_t *r, u_char *p,
-    u_char *end, size_t size, njs_uint_t ncaptures)
-{
-    u_char              c;
-    uint32_t            type;
-    njs_string_subst_t  *s;
-
-    r->substitutions = njs_arr_create(vm->mem_pool, 4,
-                                      sizeof(njs_string_subst_t));
-
-    if (njs_slow_path(r->substitutions == NULL)) {
-        return NJS_ERROR;
+    if (func_replace == NULL) {
+        ret = njs_value_to_string(vm, replace, replace);
+        if (njs_slow_path(ret != NJS_OK)) {
+            return ret;
+        }
     }
 
-    s = NULL;
+    length = njs_string_prop(&string, this);
+    search_length = njs_string_prop(&s, search);
 
-    if (size == 0) {
-        goto skip;
+    pos = njs_string_index_of(&string, &s, 0);
+    if (pos < 0) {
+        vm->retval = *this;
+        return NJS_OK;
     }
 
-copy:
-
-    if (s == NULL) {
-        s = njs_arr_add(r->substitutions);
-        if (njs_slow_path(s == NULL)) {
-            return NJS_ERROR;
+    if (func_replace == NULL) {
+        ret = njs_string_get_substitution(vm, search, this, pos, NULL, 0, NULL,
+                                          replace, &retval);
+        if (njs_slow_path(ret != NJS_OK)) {
+            return ret;
         }
 
-        s->type = NJS_SUBST_COPY;
-        s->size = size;
-        s->start = p - size;
-
     } else {
-        s->size += size;
-    }
-
-skip:
-
-    while (p < end) {
-        size = 1;
-        c = *p++;
+        arguments[0] = *search;
+        njs_set_number(&arguments[1], pos);
+        arguments[2] = *this;
 
-        if (c != '$' || p == end) {
-            goto copy;
-        }
-
-        c = *p++;
-
-        if (c == '$') {
-            s = NULL;
-            goto copy;
-        }
-
-        size = 2;
-
-        if (c >= '1' && c <= '9') {
-            type = c - '0';
-
-            if (p < end) {
-                c = *p;
-
-                if (c >= '0' && c <= '9') {
-                    type = type * 10 + (c - '0');
-                    p++;
-                    size = 3;
-                }
-            }
-
-            if (type >= ncaptures) {
-                goto copy;
-            }
+        ret = njs_function_call(vm, func_replace,
+                                njs_value_arg(&njs_value_undefined),
+                                arguments, 3, &retval);
 
-            type *= 2;
-
-        } else if (c == '`') {
-            type = NJS_SUBST_PRECEDING;
-
-        } else if (c == '&') {
-            type = 0;
-
-        } else if (c == '\'') {
-            type = NJS_SUBST_FOLLOWING;
-
-        } else {
-            goto copy;
-        }
-
-        s = njs_arr_add(r->substitutions);
-        if (njs_slow_path(s == NULL)) {
+        ret = njs_value_to_string(vm, &retval, &retval);
+        if (njs_slow_path(ret != NJS_OK)) {
             return NJS_ERROR;
         }
-
-        s->type = type;
-        s = NULL;
-    }
-
-    return NJS_OK;
-}
-
-
-static njs_int_t
-njs_string_replace_substitute(njs_vm_t *vm, njs_string_replace_t *r,
-    int *captures)
-{
-    int                        *capture;
-    uint32_t                   i, n, last;
-    const u_char               *end;
-    njs_string_subst_t         *s;
-    njs_string_replace_part_t  *part, *subject;
-
-    capture = NULL;
-
-    last = r->substitutions->items;
-    end = r->part[0].start + r->part[0].size;
-
-    part = njs_arr_add_multiple(&r->parts, last + 1);
-    if (njs_slow_path(part == NULL)) {
-        return NJS_ERROR;
-    }
-
-    r->part = &part[-1];
-
-    part[last].start = r->part[0].start + captures[1];
-
-    if (captures[1] == 0) {
-
-        /* Empty match. */
-
-        if (r->part[0].start < end) {
-            captures[1] = njs_utf8_next(r->part[0].start, end)
-                          - r->part[0].start;
-            part[last].start = r->part[0].start + captures[1];
-
-        } else {
-            /* To exit the loop. */
-            part[last].start = r->part[0].start + 1;
-        }
     }
 
-    part[last].size = r->part[0].size - captures[1];
-    njs_set_invalid(&part[last].value);
-
-    r->part[0].size = captures[0];
-
-    s = r->substitutions->start;
-
-    for (i = 0; i < last; i++) {
-        n = s[i].type;
-
-        switch (n) {
-
-        /* Literal text, "$$", and out of range "$n" substitutions. */
-        case NJS_SUBST_COPY:
-            part->start = s[i].start;
-            part->size = s[i].size;
-            break;
-
-        /* "$`" substitution. */
-        case NJS_SUBST_PRECEDING:
-            subject = r->parts.start;
-            part->start = subject->start;
-            part->size = (r->part[0].start - subject->start) + r->part[0].size;
-            break;
-
-        /* "$'" substitution. */
-        case NJS_SUBST_FOLLOWING:
-            part->start = r->part[last + 1].start;
-            part->size = r->part[last + 1].size;
-            break;
-
-        /*
-         * "$n" and "$&" substitutions.
-         */
-        default:
-            if (captures[n] == captures[n + 1]) {
-
-                /* Empty match. */
-
-                if (n > 0 && captures[n - 1] == captures[n]) {
-
-                    /*
-                     * Consecutive empty matches as in
-                     * 'ab'.replace(/(z*)(h*)/g, 'x')
-                     */
-
-                    part->size = 0;
-                    break;
-                }
-
-                capture = &captures[n];
-                continue;
-            }
-
-            if (capture != NULL) {
-
-                /*
-                 * Inserting a single character after a series of
-                 * (possibly several) empty matches.
-                 */
-
-                if (part->start < end) {
-                    part->start = r->part[0].start + *capture;
-                    part->size = njs_utf8_next(part->start, end) - part->start;
-
-                } else {
-                    part->size = 0;
-                }
-
-                capture = NULL;
-                break;
-            }
-
-            part->start = r->part[0].start + captures[n];
-            part->size = captures[n + 1] - captures[n];
-            break;
-        }
-
-        njs_set_invalid(&part->value);
-        part++;
-    }
-
-    if (capture != NULL) {
-        part->start = r->part[0].start + *capture;
-
-        if (part->start < end) {
-            part->size = njs_utf8_next(part->start, end) - part->start;
-
-        } else {
-            part->size = 0;
-        }
+    if (length == string.size) {
+        p = string.start + pos;
 
-        njs_set_invalid(&part->value);
-        part++;
+    } else {
+        /* UTF-8 string. */
+        p = njs_string_offset(string.start, string.start + string.size, pos);
     }
 
-    r->part = part;
-
-    return NJS_OK;
-}
-
-
-static njs_int_t
-njs_string_replace_join(njs_vm_t *vm, njs_string_replace_t *r)
-{
-    u_char                     *p, *string;
-    size_t                     size, length, mask;
-    ssize_t                    len;
-    njs_uint_t                 i, n;
-    njs_string_replace_part_t  *part;
-
-    size = 0;
-    length = 0;
-    mask = -1;
-
-    part = r->parts.start;
-    n = r->parts.items;
+    ret_length = njs_string_prop(&ret_string, &retval);
 
-    for (i = 0; i < n; i++) {
-        if (part[i].size == 0) {
-            continue;
-        }
-
-        size += part[i].size;
-
-        if (part[i].start == NULL) {
-            part[i].start = part[i].value.short_string.start;
-        }
-
-        len = njs_utf8_length(part[i].start, part[i].size);
-
-        if (len >= 0) {
-            length += len;
+    size = string.size + ret_string.size -  s.size;
+    length += ret_length - search_length;
 
-        } else {
-            mask = 0;
-        }
-    }
-
-    length &= mask;
-
-    string = njs_string_alloc(vm, &vm->retval, size, length);
-    if (njs_slow_path(string == NULL)) {
+    r = njs_string_alloc(vm, &vm->retval, size, length);
+    if (njs_slow_path(r == NULL)) {
         return NJS_ERROR;
     }
 
-    p = string;
-
-    for (i = 0; i < n; i++) {
-        size = part[i].size;
-
-        if (size != 0) {
-            p = njs_cpymem(p, part[i].start, size);
-        }
-
-        /* GC: release valid values. */
-    }
-
-    njs_arr_destroy(&r->parts);
+    r = njs_cpymem(r, string.start, p - string.start);
+    r = njs_cpymem(r, ret_string.start, ret_string.size);
+    r = njs_cpymem(r, p + s.size, string.size - s.size - (p - string.start));
 
     return NJS_OK;
 }
 
 
-static void
-njs_string_replacement_copy(njs_string_replace_part_t *string,
-    const njs_value_t *value)
-{
-    size_t  size;
-
-    string->value = *value;
-
-    size = value->short_string.size;
-
-    if (size != NJS_STRING_LONG) {
-        string->start = NULL;
-
-    } else {
-        string->start = value->long_string.data->start;
-        size = value->long_string.size;
-    }
-
-    string->size = size;
-}
-
-
 double
 njs_string_to_number(const njs_value_t *value, njs_bool_t parse_float)
 {
index c78510cae5af66864eaa572e274d138af044da06..da300a4959e3268e6c397ceadd3cf313b31387c2 100644 (file)
@@ -199,6 +199,9 @@ njs_index_t njs_value_index(njs_vm_t *vm, const njs_value_t *src,
 
 njs_int_t njs_string_prototype_concat(njs_vm_t *vm, njs_value_t *args,
     njs_uint_t nargs, njs_index_t unused);
+njs_int_t njs_string_get_substitution(njs_vm_t *vm, njs_value_t *matched,
+    njs_value_t *string, int64_t pos, njs_value_t *captures, int64_t ncaptures,
+    njs_value_t *groups, njs_value_t *replacement, njs_value_t *retval);
 
 
 extern const njs_object_init_t  njs_string_instance_init;
index 89b64ff830fedf5b4d236bd4235928e183f8f0c7..4d5aba6d54b574355daebcf26e9f9e9cda5ce744 100644 (file)
@@ -1519,3 +1519,28 @@ default_constructor:
 
     return NJS_OK;
 }
+
+
+njs_int_t
+njs_value_method(njs_vm_t *vm, njs_value_t *value, njs_value_t *key,
+    njs_value_t *retval)
+{
+    njs_int_t  ret;
+
+    ret = njs_value_to_object(vm, value);
+    if (njs_slow_path(ret != NJS_OK)) {
+        return ret;
+    }
+
+    ret = njs_value_property(vm, value, key, retval);
+    if (njs_slow_path(ret != NJS_OK)) {
+        return (ret == NJS_DECLINED) ? NJS_OK : ret;
+    }
+
+    if (njs_slow_path(!njs_is_function(retval))) {
+        njs_type_error(vm, "method is not callable");
+        return NJS_ERROR;
+    }
+
+    return NJS_OK;
+}
index 726ce717c64928a1930697bfb3ee08d4ee82ed7f..609eadc4512542b8999121bc6d08d4905bafc5f6 100644 (file)
@@ -1030,6 +1030,9 @@ void njs_symbol_conversion_failed(njs_vm_t *vm, njs_bool_t to_string);
 njs_int_t njs_value_species_constructor(njs_vm_t *vm, njs_value_t *object,
     njs_value_t *default_constructor, njs_value_t *dst);
 
+njs_int_t njs_value_method(njs_vm_t *vm, njs_value_t *value, njs_value_t *key,
+    njs_value_t *retval);
+
 
 njs_inline njs_int_t
 njs_value_property_i64(njs_vm_t *vm, njs_value_t *value, int64_t index,
index 82079ed20e09eb82a3b51ea6b3e19f3c63f966b9..545188712daf4de9603e533bee2d9a3570a023de 100644 (file)
@@ -7171,6 +7171,9 @@ static njs_unit_test_t  njs_test[] =
     { njs_str("''.indexOf.call(12345, 45, '0')"),
       njs_str("3") },
 
+    { njs_str("var r = new String('undefined').indexOf(x); var x; r"),
+      njs_str("0") },
+
     { njs_str("'abc'.lastIndexOf('abcdef')"),
       njs_str("-1") },
 
@@ -7448,82 +7451,159 @@ static njs_unit_test_t  njs_test[] =
                  "'123456'.search(r)"),
       njs_str("3") },
 
-    { njs_str("'abcdefgh'.replace()"),
-      njs_str("abcdefgh") },
+    { njs_str("'abc'.replace()"),
+      njs_str("abc") },
 
-    { njs_str("'abcdefgh'.replace('d')"),
-      njs_str("abcundefinedefgh") },
+    { njs_str("'ABC'.replace('B')"),
+      njs_str("AundefinedC") },
 
-    { njs_str("'abcdefgh'.replace('d', undefined)"),
-      njs_str("abcundefinedefgh") },
+    { njs_str("'ABC'.replace('B', undefined)"),
+      njs_str("AundefinedC") },
 
     { njs_str("'a'.repeat(16).replace('a'.repeat(17)) === 'a'.repeat(16)"),
       njs_str("true") },
 
-    { njs_str("'abcdefgh'.replace('d', null)"),
-      njs_str("abcnullefgh") },
+    { njs_str("'α'.repeat(16).replace('α'.repeat(17)) === 'α'.repeat(16)"),
+      njs_str("true") },
 
-    { njs_str("'abcdefgh'.replace('d', 1)"),
-      njs_str("abc1efgh") },
+    { njs_str("'ABC'.replace('B', null)"),
+      njs_str("AnullC") },
 
-    { njs_str("'abcdefghdijklm'.replace('d', 'X')"),
-      njs_str("abcXefghdijklm") },
+    { njs_str("'abc'.replace('c', 1)"),
+      njs_str("ab1") },
 
-    { njs_str("'абвгдежгийклм'.replace('г', 'Г')"),
-      njs_str("абвГдежгийклм") },
+    { njs_str("'abc'.replace('a', 'X')"),
+      njs_str("Xbc") },
 
-    { njs_str("'abcdefghdijklm'.replace('d',"
-                 "   function(m, o, s) { return '|'+s+'|'+o+'|'+m+'|' })"),
-      njs_str("abc|abcdefghdijklm|3|d|efghdijklm") },
+    { njs_str("'abc'.replace('b', 'X')"),
+      njs_str("aXc") },
 
-    { njs_str("'abcdefgh'.replace('', 'X')"),
-      njs_str("Xabcdefgh") },
+    { njs_str("('a'.repeat(33) + 'bb').replace('bb', 'CC').slice(31)"),
+      njs_str("aaCC") },
 
-    { njs_str("'abcdefghdijklm'.replace(/d/, 'X')"),
-      njs_str("abcXefghdijklm") },
+    { njs_str("var r = 'abc'.replace('c', 'X'); [r, r.length]"),
+      njs_str("abX,3") },
 
-    { njs_str("'abcdefghdijklm'.replace(/d/,"
-                 "   function(m, o, s) { return '|'+s+'|'+o+'|'+m+'|' })"),
-      njs_str("abc|abcdefghdijklm|3|d|efghdijklm") },
+    { njs_str("var r = 'αβγ'.replace('α', 'X'); [r, r.length]"),
+      njs_str("Xβγ,3") },
 
-    { njs_str("'abcdefghdijklm'.replace(/(d)/,"
-                 "   function(m, p, o, s)"
-                       "{ return '|'+s+'|'+o+'|'+m+'|'+p+'|' })"),
-      njs_str("abc|abcdefghdijklm|3|d|d|efghdijklm") },
+    { njs_str("var r = 'αβγ'.replace('β', 'X'); [r, r.length]"),
+      njs_str("αXγ,3") },
 
-    { njs_str("'abc'.replace(/b/, ()=>1)"),
-      njs_str("a1c") },
+    { njs_str("var r = 'αβγ'.replace('γ', 'X'); [r, r.length]"),
+      njs_str("αβX,3") },
 
-    { njs_str("var n = 0; 'abbbc'.replace(/b/g, function() {return ++n;})"),
-      njs_str("a123c") },
+    { njs_str("var r = 'αβγ'.replace('', 'X'); [r, r.length]"),
+      njs_str("Xαβγ,4") },
+
+    { njs_str("'abc'.replace('b', (m, o, s) => `|${s}|${o}|${m}|`)"),
+      njs_str("a|abc|1|b|c") },
+
+    { njs_str("'abcdbe'.replace('b', '|$`X$\\'|')"),
+      njs_str("a|aXcdbe|cdbe") },
+
+    { njs_str("'undefined'.replace(void 0, 'x')"),
+      njs_str("x") },
+
+    { njs_str("'12345'.replace(3, () => 0)"),
+      njs_str("12045") },
+
+    { njs_str("var r = new String('undefined').replace(x, Function('return arguments[1]+42;')); var x; r"),
+      njs_str("42") },
+
+    { njs_str("'123'.replace(3, function() { return {toString: ()=>({})}; })"),
+      njs_str("TypeError: Cannot convert object to primitive value") },
+
+    { njs_str("'12345'.replace(3, () => ({toString: () => 'aaaa'}))"),
+      njs_str("12aaaa45") },
+
+    { njs_str("'abc'.replace(/a/, 'X')"),
+      njs_str("Xbc") },
+
+    { njs_str("'abccd'.replace(/c/, 'X')"),
+      njs_str("abXcd") },
 
-    { njs_str("'abcdefghdijklm'.replace(/x/, 'X')"),
-      njs_str("abcdefghdijklm") },
+    { njs_str("'abc'.replace(/c/, 'X')"),
+      njs_str("abX") },
 
-    { njs_str("'abcdefghdijklm'.replace(/x/,"
-                 "   function(m, o, s) { return '|'+s+'|'+o+'|'+m+'|' })"),
-      njs_str("abcdefghdijklm") },
+    { njs_str("'abccd'.replace(/c+/, 'X')"),
+      njs_str("abXd") },
 
-    { njs_str("'абвгдежгийклм'.replace(/г/, 'Г')"),
-      njs_str("абвГдежгийклм") },
+    { njs_str("'abc'.replace(/f/, 'X')"),
+      njs_str("abc") },
+
+    { njs_str("('a'.repeat(33) + 'bb').replace(/bb/, 'CC').slice(31)"),
+      njs_str("aaCC") },
+
+    { njs_str("'abccd'.replace(/c/g, 'X')"),
+      njs_str("abXXd") },
+
+    { njs_str("('a'.repeat(33) + 'bb').replace(/bb/g, 'CC').slice(31)"),
+      njs_str("aaCC") },
+
+    { njs_str("'abccd'.replace(/[ac]/g, 'X')"),
+      njs_str("XbXXd") },
+
+    { njs_str("'ab'.replace(/q*/g, 'X')"),
+      njs_str("XaXbX") },
+
+    { njs_str("'αβ'.replace(/q*/g, 'X')"),
+      njs_str("XαXβX") },
 
-    { njs_str("'abcdefghdijklm'.replace(/d/g, 'X')"),
-      njs_str("abcXefghXijklm") },
+    { njs_str("'αβ'.replace(/(q)*/g, 'X')"),
+      njs_str("XαXβX") },
+
+    { njs_str("'αβ'.replace(/q*/g, 'γ')"),
+      njs_str("γαγβγ") },
+
+    { njs_str("':α:β:γ:'.replace(/:/g, '')"),
+      njs_str("αβγ") },
+
+    { njs_str("':α:β:γ:'.replace(/[αβγ]/g, '')"),
+      njs_str("::::") },
+
+    { njs_str("'aabbccaa'.replace(/a*/g, '')"),
+      njs_str("bbcc") },
 
-    { njs_str("'абвгдежгийклм'.replace(/г/g, 'Г')"),
-      njs_str("абвГдежГийклм") },
+    { njs_str("'aabbccaab'.replace(/z*/g, '')"),
+      njs_str("aabbccaab") },
+
+    { njs_str("''.replace(/a*/g, '')"),
+      njs_str("") },
+
+    { njs_str("'abcde'.replace(/d/, (m, o, s) => `|${s}|${o}|${m}|`)"),
+      njs_str("abc|abcde|3|d|e") },
+
+    { njs_str("'abcde'.replace(/(d)/, (m, p, o, s) => `|${s}|${o}|${m}|${p}|`)"),
+      njs_str("abc|abcde|3|d|d|e") },
+
+    { njs_str("'abc'.replace(/b/, () => 1)"),
+      njs_str("a1c") },
+
+    { njs_str("var n = 0; 'abbbc'.replace(/b/g, () => ++n)"),
+      njs_str("a123c") },
+
+    { njs_str("'abc'.replace(/x/, (m, o, s) => `|${s}|${o}|${m}|`)"),
+      njs_str("abc") },
 
     { njs_str("'abc12345#$*%'.replace(/([^\\d]*)(\\d*)([^\\w]*)/,"
-                 "   function(match, p1, p2, p3) {"
-                 "     return [p1, p2, p3].join('-')})"),
+              "                       (_, p1, p2, p3) => [p1, p2, p3].join('-'))"),
       njs_str("abc-12345-#$*%") },
 
-    { njs_str("'ABCDEFGHDIJKLM'.replace(/[A-Z]/g,"
-                 "   function(match) { return '-' + match.toLowerCase() })"),
-      njs_str("-a-b-c-d-e-f-g-h-d-i-j-k-l-m") },
+    { njs_str("'abc'.replace(/(?<named>b)/, (m, p, o, s, gr) => `|${gr.named}|`)"),
+      njs_str("a|b|c") },
+
+    { njs_str("'ABC'.replace(/[A-Z]/g, m => '-' + m.toLowerCase())"),
+      njs_str("-a-b-c") },
+
+    { njs_str("'abc'.replace(/(b)c/g, '|$01|')"),
+      njs_str("a|b|") },
+
+    { njs_str("'abc'.replace(/(b)c/g, '@$0|$01|$00@')"),
+      njs_str("a@$0|b|$00@") },
 
-    { njs_str("'abcdbe'.replace(/(b)/g, '$')"),
-      njs_str("a$cd$e") },
+    { njs_str("'abcdeFGHIJ'.replace(/(a)(b)(c)(d)(e)(F)(G)(H)(I)(J)/, '$9|$10|$11|$01')"),
+      njs_str("I|J|a1|a") },
 
     { njs_str("'abcdbe'.replace(/(b)/g, '$2$23')"),
       njs_str("a$2$23cd$2$23e") },
@@ -7531,72 +7611,50 @@ static njs_unit_test_t  njs_test[] =
     { njs_str("'abcdbe'.replace(/(b)/g, '$2$23X$$Y')"),
       njs_str("a$2$23X$Ycd$2$23X$Ye") },
 
-    { njs_str("'abcdbe'.replace('b', '|$`X$\\'|')"),
-      njs_str("a|aXcdbe|cdbe") },
-
     { njs_str("'abcdbe'.replace(/b/, '|$`X$\\'|')"),
       njs_str("a|aXcdbe|cdbe") },
 
     { njs_str("'abcdbefbgh'.replace(/b/g, '|$`X$\\'|')"),
       njs_str("a|aXcdbefbgh|cd|abcdXefbgh|ef|abcdbefXgh|gh") },
 
-    { njs_str("'abc12345#$*%'.replace(/([^\\d]*)(\\d*)([^\\w]*)/,"
-                 "                       '$1-$2-$3')"),
+    { njs_str("'abc12345#$*%'.replace(/([^\\d]*)(\\d*)([^\\w]*)/, '$1-$2-$3')"),
       njs_str("abc-12345-#$*%") },
 
     { njs_str("'$1,$2'.replace(/(\\$(\\d))/g, '$$1-$1$2')"),
       njs_str("$1-$11,$1-$22") },
 
-    { njs_str("('β' + 'α'.repeat(33)+'β').replace(/(α+)(β+)/, function(m, p1) { return p1[32]; })"),
-      njs_str("βα") },
+    { njs_str("'ABC'.replace(/(h*)(z*)(g*)/g, '$1@$2α$3')"),
+      njs_str("@αA@αB@αC@α") },
 
-    { njs_str("'abc'.replace(/(h*)(z*)(g*)/g, '$1nn$2zz$3')"),
-      njs_str("nnzzannzzbnnzzcnnzz") },
+    { njs_str("'abc'.replace(/(h*)(z*)/g, '$1@$2#$3:')"),
+      njs_str("@#$3:a@#$3:b@#$3:c@#$3:") },
 
-    { njs_str("'abc'.replace(/(h*)(z*)/g, '$1nn$2zz$3yy')"),
-      njs_str("nnzz$3yyannzz$3yybnnzz$3yycnnzz$3yy") },
+    { njs_str("/b(c)(z)?(.)/[Symbol.replace]('abcde', '[$1$2$3]')"),
+      njs_str("a[cd]e") },
 
-    { njs_str("'ъ'.replace(/(h*)/g, '$1ЮЙ')"),
-      njs_str("ЮЙъЮЙ") },
+    { njs_str("/b(c)(z)?(.)/[Symbol.replace]('abcde', '[$01$02$03$04$00]')"),
+      njs_str("a[cd$04$00]e") },
 
-    { njs_str("'ъg'.replace(/(h*)/g, '$1ЮЙ')"),
-      njs_str("ЮЙъЮЙgЮЙ") },
+    { njs_str("'α'.replace(/(h*)/g, '$1βγ')"),
+      njs_str("βγαβγ") },
 
-    { njs_str("'ъg'.replace(/(ъ*)/g, '$1ЮЙ')"),
-      njs_str("ъЮЙЮЙgЮЙ") },
+    { njs_str("'αg'.replace(/(h*)/g, '$1βγ')"),
+      njs_str("βγαβγgβγ") },
 
-    { njs_str("'ъg'.replace(/(h*)/g, 'fg$1ЮЙ')"),
-      njs_str("fgЮЙъfgЮЙgfgЮЙ") },
+    { njs_str("'αg'.replace(/(α*)/g, '$1βγ')"),
+      njs_str("αβγβγgβγ") },
 
-    { njs_str("'юgёfя'.replace(/(gё)/g, 'n$1i')"),
-      njs_str("юngёifя") },
+    { njs_str("'αg'.replace(/(h*)/g, 'fg$1βγ')"),
+      njs_str("fgβγαfgβγgfgβγ") },
 
-    { njs_str("'aabbccaa'.replace(/a*/g, '')"),
-      njs_str("bbcc") },
-
-    { njs_str("'aabbccaab'.replace(/z*/g, '')"),
-      njs_str("aabbccaab") },
-
-    { njs_str("'αβγ'.replace(/z*/g, '|')"),
-      njs_str("|α|β|γ|") },
-
-    { njs_str("''.replace(/a*/g, '')"),
-      njs_str("") },
-
-    { njs_str("'12345'.replace(3, () => 0)"),
-      njs_str("12045") },
-
-    { njs_str("'123'.replace(3, function() { return {toString: ()=>({})}; })"),
-      njs_str("TypeError: Cannot convert object to primitive value") },
-
-    { njs_str("'12345'.replace(3, () => ({toString: () => 'aaaa'}))"),
-      njs_str("12aaaa45") },
+    { njs_str("'αgβfγ'.replace(/(gβ)/g, 'n$1i')"),
+      njs_str("αngβifγ") },
 
-    { njs_str("'abc'.replace(/(z*)/g, function v0() {return '124'})"),
-      njs_str("124a124b124c124") },
+    { njs_str("'abc'.replace(/b/g, '|$&|')"),
+      njs_str("a|b|c") },
 
-    { njs_str("'abc'.replace(/(a*)/g, function v0() {return '124'})"),
-      njs_str("124124b124c124") },
+    { njs_str("'ABC'.replace(/((A)B)/g, '($1|$&|$2)')"),
+      njs_str("(AB|AB|A)C") },
 
     { njs_str("'abc'.replace(/b/g, '$0')"),
       njs_str("a$0c") },
@@ -7604,19 +7662,51 @@ static njs_unit_test_t  njs_test[] =
     { njs_str("typeof String.bytesFrom(Array(15).fill(0xE3)).replace(/^/g, 1)"),
       njs_str("string") },
 
-#if 0 /* FIXME: PCRE limitation */
     { njs_str("'abc'.replace(/^/g, '|$&|')"),
       njs_str("||abc") },
-#endif
 
-    { njs_str("'abc'.replace(/b/g, '|$&|')"),
-      njs_str("a|b|c") },
+    { njs_str("var uri ='/u/v1/Aa/bB?type=m3u8&mt=42';"
+              "uri.replace(/^\\/u\\/v1\\/[^/]*\\/([^\?]*)\\?.*(mt=[^&]*).*$/, '$1|$2')"),
+      njs_str("bB|mt=42") },
 
-    { njs_str("'ABC'.replace(/((A)B)/g, '($1|$&|$2)')"),
-      njs_str("(AB|AB|A)C") },
+    { njs_str("'ABC'.replace(/(?<b>B)/, '|$<b>|@$<a>@')"),
+      njs_str("A|B|@@C") },
 
-    { njs_str("'undefined'.replace(void 0, 'x')"),
-      njs_str("x") },
+    { njs_str("'ABC'.replace(/(?<b>B)/, '|$<BB|')"),
+      njs_str("A|$<BB|C") },
+
+    { njs_str("'ABC'.replace(/(?<b>B)/, '|$<BB$$|>@')"),
+      njs_str("A|@C") },
+
+    { njs_str("('β' + 'α'.repeat(33)+'β').replace(/(α+)(β+)/, (m, p1) => p1[32])"),
+      njs_str("βα") },
+
+    { njs_str("'abc'.replace(/(z*)/g, () => '@')"),
+      njs_str("@a@b@c@") },
+
+    { njs_str("'abc'.replace(/(a*)/g, () => '@')"),
+      njs_str("@@b@c@") },
+
+    { njs_str("var O = RegExp.prototype[Symbol.replace];"
+              "RegExp.prototype[Symbol.replace] = function (s, rep) { return O.call(this, s, `|${rep}|`); };"
+              "'ABC'.replace(/B/, '+')"),
+      njs_str("A|+|C") },
+
+    { njs_str("var O = RegExp.prototype.exec;"
+              "function mangled(s) { var r = O.call(this, s); Object.defineProperty(r, '0', {enumerable:false}); "
+              "                      return r; };"
+              "RegExp.prototype.exec = mangled;"
+              "'ABC'.replace(/(B)/, (m, p1, off, s) => `@${m}|${p1}|${off}|${s}@`)"),
+      njs_str("A@B|B|1|ABC@C") },
+
+    { njs_str("RegExp.prototype[Symbol.replace].call()"),
+      njs_str("TypeError: \"this\" is not object") },
+
+    { njs_str("RegExp.prototype[Symbol.replace].call(1)"),
+      njs_str("TypeError: \"this\" is not object") },
+
+    { njs_str("RegExp.prototype[Symbol.replace].call(/b/, 'abc','B')"),
+      njs_str("aBc") },
 
     { njs_str("/]/"),
       njs_str("/\\]/") },
@@ -9534,6 +9624,9 @@ static njs_unit_test_t  njs_test[] =
     { njs_str("/./['exec'] === RegExp.prototype.exec"),
       njs_str("true") },
 
+    { njs_str("/./[Symbol.replace] === RegExp.prototype[Symbol.replace]"),
+      njs_str("true") },
+
     { njs_str("/^[A-Za-z0-9+/]{4}$/.test('////')"),
       njs_str("true") },
 
@@ -9697,14 +9790,15 @@ static njs_unit_test_t  njs_test[] =
     { njs_str("var r = /a/.exec('a'); ['groups' in r, typeof r.groups]"),
       njs_str("true,undefined") },
 
-#if (!NJS_HAVE_MEMORY_SANITIZER) /* PCRE bug in groups code */
     { njs_str("var r = /(?<m>[0-9]{2})\\/(?<d>[0-9]{2})\\/(?<y>[0-9]{4})/;"
-                 "var g = r.exec('12/31/1986').groups;"
-                 "g.d + '.' + g.m + '.' + g.y"),
+              "var g = r.exec('12/31/1986').groups;"
+              "g.d + '.' + g.m + '.' + g.y"),
       njs_str("31.12.1986") },
 
+#if (!NJS_HAVE_MEMORY_SANITIZER) /* PCRE bug in groups code */
+
     { njs_str("var g = /(?<r>(?<no>no)?(?<yes>yes)?)/.exec('yes').groups;"
-                 "[Object.keys(g).length,'no' in g, typeof g.no, g.yes, g.r]"),
+              "[Object.keys(g).length,'no' in g, typeof g.no, g.yes, g.r]"),
       njs_str("3,true,undefined,yes,yes") },
 #endif
 
@@ -17792,6 +17886,7 @@ static njs_unit_test_t  njs_shell_test[] =
               "              function(m) {return m.a.a})" ENTER),
       njs_str("TypeError: cannot get property \"a\" of undefined\n"
               "    at anonymous (:1)\n"
+              "    at RegExp.prototype[Symbol.replace] (native)\n"
               "    at String.prototype.replace (native)\n"
               "    at main (:1)\n") },