├── .editorconfig ├── .gitignore ├── .nvmrc ├── .travis-github-deploy-key.enc ├── .travis.yml ├── README.md ├── package.json └── spec.html /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = tab 6 | end_of_line = lf 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | 10 | [{README.md,package.json,spec.html,.travis.yml}] 11 | indent_style = space 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | package-lock.json 2 | 3 | dist 4 | 5 | # Installed npm modules 6 | node_modules 7 | 8 | # Folder view configuration files 9 | .DS_Store 10 | Desktop.ini 11 | 12 | # Thumbnail cache files 13 | ._* 14 | Thumbs.db 15 | 16 | # Files that might appear on external disks 17 | .Spotlight-V100 18 | .Trashes 19 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 10 2 | -------------------------------------------------------------------------------- /.travis-github-deploy-key.enc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tc39/proposal-string-replaceall/c64a7e62e3288a8cd4f7a4e3f1145545b47f9e7b/.travis-github-deploy-key.enc -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | after_success: 3 | - $(npm bin)/set-up-ssh --key "${encrypted_57be495982e6_key}" --iv "${encrypted_57be495982e6_iv}" 4 | --path-encrypted-key .travis-github-deploy-key.enc 5 | - $(npm bin)/update-branch --commands 'npm run build' --commit-message "Update gh-pages 6 | @ ${TRAVIS_COMMIT}" --directory 'dist' --distribution-branch 'gh-pages' --source-branch 7 | 'master' 8 | branches: 9 | only: 10 | - master 11 | env: 12 | global: 13 | secure: DR+3vbfdRzwV8aoaV0TRnc9a9uR+W/T40gUkrIvaZdDAfz1KRxeeZ7txdATtBG9AHdSOqiTr88uM5zbmbA9W00IyoL2DVa2KrVQNwKLdIcWxr3wiGsM6tnplbepoAe2/zpPtgKYtiHNBAqpyCg1k4bNUjRs/omT7uIPcYDdJU14UnoWq0+fBy+OrOvO1lShx4Loy1c8vqP2DEBAGUXMENVWBMH9eTd+zzneSXt9IWmuIBgisqpHBB4fX+EFyBbIHA/vJVolGjKIka2xX76Ixbef1KBOQf3LfyDuNWpZEKrKyShG42SuTxFjtVfxnvotQsWi/u72u59nHnb/MIi0IjRkx4yKl74hk4QEA/sC2s4bfMRw/ZXoHVEYwDCAZgkeyfSNGr6e4pzxkZ/KHhMcxNKQE2GdSK0V/1SbxeAgJoM5eTSoam4LMPhDCyGT7pIs999dZcYG4hqBVA8zb62emto0Giwwxi7IE5Qb3qWZQEUGREYxCMH6t1nMkmNx1pAIz9AN+itInobaTp15eptSHrvJ/W6JmrwfYYHzc2G3R/lwl/7AmzKnsqvkSngL6Pipj+7Hb4edMDCJ4WUZ88+udEBVwxKPDxbXgjOiOTr5cQWB+Bun5fufLVy+vLM7UGo8JvXo2JtgAOLxP1ev6jWhVgnVUEV8lWoiIEygRcG1wAFs= 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `String.prototype.replaceAll` proposal 2 | 3 | ## Status 4 | 5 | Champion: Mathias Bynens (Google, @mathiasbynens). 6 | 7 | This proposal is at stage 4 of [the TC39 process](https://tc39.es/process-document/), and is scheduled to be included in ES2021. 8 | 9 | ## Motivation 10 | 11 | (Also see [our TL;DR explainer](https://v8.dev/features/string-replaceall).) 12 | 13 | Currently there is no way to replace all instances of a substring in a string without use of a global regexp. 14 | `String.prototype.replace` only affects the first occurrence when used with a string argument. There is a lot of evidence that developers are trying to do this in JS — see the [StackOverflow question](https://stackoverflow.com/q/1144783/96656) with thousands of votes. 15 | 16 | Currently the most common way of achieving this is to use a global regexp. 17 | 18 | ```js 19 | const queryString = 'q=query+string+parameters'; 20 | const withSpaces = queryString.replace(/\+/g, ' '); 21 | ``` 22 | 23 | This approach has the downside of requiring special RegExp characters to be escaped — note the escaped `'+'`. 24 | 25 | An alternate solution is to combine `String#split` with `Array#join`: 26 | 27 | ```js 28 | const queryString = 'q=query+string+parameters'; 29 | const withSpaces = queryString.split('+').join(' '); 30 | ``` 31 | 32 | This approach avoids any escaping but comes with the overhead of splitting the string into an array of parts only to glue it back together. 33 | 34 | ## Proposed solution 35 | 36 | We propose the addition of a new method to the String prototype - `replaceAll`. This would give developers a straight-forward way to accomplish this common, basic operation. 37 | 38 | ```js 39 | const queryString = 'q=query+string+parameters'; 40 | const withSpaces = queryString.replaceAll('+', ' '); 41 | ``` 42 | 43 | It also removes the need to escape special regexp characters (note the unescaped `'+'`). 44 | 45 | ## High-level API 46 | 47 | The proposed signature is the same as the existing `String.prototype.replace` method: 48 | 49 | ```js 50 | String.prototype.replaceAll(searchValue, replaceValue) 51 | ``` 52 | 53 | Per the current TC39 consensus, `String.prototype.replaceAll` behaves identically to `String.prototype.replace` in all cases, **except** for the following two cases: 54 | 55 | 1. If `searchValue` is a string, `String.prototype.replace` only replaces a single occurrence of the `searchValue`, whereas `String.prototype.replaceAll` replaces *all* occurrences of the `searchValue` (as if `.split(searchValue).join(replaceValue)` or a global & properly-escaped regular expression had been used). 56 | 1. If `searchValue` is a non-global regular expression, `String.prototype.replace` replaces a single match, whereas `String.prototype.replaceAll` throws an exception. This is done to avoid the inherent confusion between the lack of a global flag (which implies "do NOT replace all") and the name of the method being called (which strongly suggests "replace all"). 57 | 58 | Notably, `String.prototype.replaceAll` behaves just like `String.prototype.replace` if `searchValue` is a global regular expression. 59 | 60 | ## Comparison to other languages 61 | 62 | * Java has [`replace`](https://docs.oracle.com/javase/8/docs/api/java/lang/String.html#replace-java.lang.CharSequence-java.lang.CharSequence-), accepting a `CharSequence` and replacing all occurrences. Java also has [`replaceAll`](https://docs.oracle.com/javase/8/docs/api/java/lang/String.html#replaceAll-java.lang.String-java.lang.String-) which accepts a regex as the search term (requiring the user to escape special regex characters), again replacing all occurrences by default. 63 | * Python [`replace`](https://www.tutorialspoint.com/python/string_replace.htm) replaces all occurrences, but accepts an optional param to limit the number of replacements. 64 | * PHP has [`str_replace`](http://php.net/manual/en/function.str-replace.php) which has an optional limit parameter like python. 65 | * Ruby has [`gsub`](https://ruby-doc.org/core/String.html#method-i-gsub), accepting a regexp or string, but also accepts a callback block and a hash of match -> replacement pairs. 66 | 67 | ## FAQ 68 | 69 | ### What are the main benefits? 70 | 71 | A simplified API for this common use-case that does not require RegExp knowledge. A way to global-replace strings without having to escape RegExp syntax characters. Possibly improved optimization potential on the VM side. 72 | 73 | ### What about adding a `limit` parameter to `replace` instead? 74 | 75 | A: This is an awkward interface — because the default limit is 1, the user would have to know how many occurrences already exist, or use something like Infinity. 76 | 77 | ### What happens if `searchValue` is the empty string? 78 | 79 | `String.prototype.replaceAll` follows the precedent set by `String.prototype.replace`, and returns the input string with the replacement value spliced in between every UCS-2/UTF-16 code unit. 80 | 81 | ```js 82 | 'x'.replace('', '_'); 83 | // → '_x' 84 | 'xxx'.replace(/(?:)/g, '_'); 85 | // → '_x_x_x_' 86 | 'xxx'.replaceAll('', '_'); 87 | // → '_x_x_x_' 88 | ``` 89 | 90 | ## TC39 meeting notes 91 | 92 | - [November 2017](https://tc39.es/tc39-notes/2017-11_nov-28.html#10ih-stringprototypereplaceall-for-stage-1) 93 | - [March 2019](https://github.com/tc39/tc39-notes/blob/master/meetings/2019-03/mar-26.md#stringprototypereplaceall-for-stage-2) 94 | - [July 2019](https://github.com/tc39/tc39-notes/blob/master/meetings/2019-07/july-24.md#stringprototypereplaceall) 95 | - [June 2020](https://github.com/tc39/notes/blob/master/meetings/2020-06/june-2.md#stringprototypereplaceall-for-stage-4) 96 | 97 | ## Specification 98 | 99 | - [Ecmarkup source](https://github.com/tc39/proposal-string-replaceall/blob/master/spec.html) 100 | - [HTML version](https://tc39.es/proposal-string-replaceall/) 101 | 102 | ## Implementations 103 | 104 | - [SpiderMonkey](https://bugzilla.mozilla.org/show_bug.cgi?id=1540021), shipping in [Firefox 77](https://bugzilla.mozilla.org/show_bug.cgi?id=1608168) 105 | - [JavaScriptCore](https://bugs.webkit.org/show_bug.cgi?id=202471), shipping in [Safari 13.1](https://webkit.org/blog/10247/new-webkit-features-in-safari-13-1/#javascript-improvements) 106 | - [V8](https://bugs.chromium.org/p/v8/issues/detail?id=9801), shipping in Chrome 85 107 | - Polyfills: 108 | - [core-js](https://github.com/zloirock/core-js#stringreplaceall) 109 | - [es-shims](https://github.com/es-shims/String.prototype.replaceAll) 110 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "test": ":", 5 | "build": "mkdir -p dist; ecmarkup --verbose spec.html dist/index.html --css dist/ecmarkup.css --js dist/ecmarkup.js" 6 | }, 7 | "devDependencies": { 8 | "@alrra/travis-scripts": "^3.0.1", 9 | "ecmarkup": "^3.16.0" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /spec.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
  4 | title: String.prototype.replaceAll
  5 | status: proposal
  6 | stage: 4
  7 | location: https://tc39.es/proposal-string-replace-all/
  8 | copyright: false
  9 | contributors: Jakob Gruber, Mathias Bynens
 10 | 
11 | 12 | 13 | 25 | 26 | 27 |

Static Semantics: StringIndexOf ( _string_, _searchValue_, _fromIndex_ )

28 |

The abstract operation StringIndexOf takes arguments _string_ (a String), _searchValue_ (a String), and _fromIndex_ (a non-negative integer). It performs the following steps when called:

29 | 30 | 1. Assert: Type(_string_) is String. 31 | 1. Assert: Type(_searchValue_) is String. 32 | 1. Assert: ! IsNonNegativeInteger(_fromIndex_) is *true*. 33 | 1. Let _len_ be the length of _string_. 34 | 1. If _searchValue_ is the empty String, and _fromIndex_ ≤ _len_, return _fromIndex_. 35 | 1. Let _searchLen_ be the length of _searchValue_. 36 | 1. If there exists any integer _k_ such that _fromIndex_ ≤ _k_ ≤ _len_ - _searchLen_ and for all nonnegative integers _j_ less than _searchLen_, the code unit at index _k_ + _j_ within _string_ is the same as the code unit at index _j_ within _searchValue_, let _pos_ be the smallest (closest to *-∞*) such integer. Otherwise, let _pos_ be -1. 37 | 1. Return _pos_. 38 | 39 | 40 |

If _searchValue_ is empty and _fromIndex_ is less than or equal to the length of _string_, this algorithm returns _fromIndex_. An empty _searchValue_ is effectively found at every position within a string, including after the last code unit.

41 |
42 | 43 |

The above algorithm accepts out-of-bounds _fromIndex_ values (where _fromIndex_ ≥ the length of _string_).

44 |
45 |
46 | 47 | 48 |

String.prototype.replaceAll ( _searchValue_, _replaceValue_ )

49 |

When the `replaceAll` method is called with arguments _searchValue_ and _replaceValue_, the following steps are taken:

50 | 51 | 1. Let _O_ be ? RequireObjectCoercible(*this* value). 52 | 1. If _searchValue_ is neither *undefined* nor *null*, then 53 | 1. Let _isRegExp_ be ? IsRegExp(_searchValue_). 54 | 1. If _isRegExp_ is true, then 55 | 1. Let _flags_ be ? Get(_searchValue_, *"flags"*). 56 | 1. Perform ? RequireObjectCoercible(_flags_). 57 | 1. If ? ToString(_flags_) does not contain *"g"*, throw a *TypeError* exception. 58 | 1. Let _replacer_ be ? GetMethod(_searchValue_, @@replace). 59 | 1. If _replacer_ is not *undefined*, then 60 | 1. Return ? Call(_replacer_, _searchValue_, « _O_, _replaceValue_ »). 61 | 1. Let _string_ be ? ToString(_O_). 62 | 1. Let _searchString_ be ? ToString(_searchValue_). 63 | 1. Let _functionalReplace_ be IsCallable(_replaceValue_). 64 | 1. If _functionalReplace_ is *false*, then 65 | 1. Set _replaceValue_ to ? ToString(_replaceValue_). 66 | 67 | 1. Let _searchLength_ be the length of _searchString_. 68 | 1. Let _advanceBy_ be max(1, _searchLength_). 69 | 1. Let _matchPositions_ be a new empty List. 70 | 1. Let _position_ be ! StringIndexOf(_string_, _searchString_, 0). 71 | 1. Repeat, while _position_ is not -1, 72 | 1. Append _position_ to the end of _matchPositions_. 73 | 1. Set _position_ to ! StringIndexOf(_string_, _searchString_, _position_ + _advanceBy_). 74 | 75 | 1. Let _endOfLastMatch_ be 0. 76 | 1. Let _result_ be the empty String value. 77 | 1. For each _position_ in _matchPositions_, do 78 | 1. If _functionalReplace_ is *true*, then 79 | 1. Let _replacement_ be ? ToString(? Call(_replaceValue_, *undefined*, « _searchString_, _position_, _string_ »)). 80 | 1. Else, 81 | 1. Assert: Type(_replaceValue_) is String. 82 | 1. Let _captures_ be a new empty List. 83 | 1. Let _replacement_ be GetSubstitution(_searchString_, _string_, _position_, _captures_, *undefined*, _replaceValue_). 84 | 1. Let _stringSlice_ be the substring of _string_ consisting of the code units from _endOfLastMatch_ (inclusive) up to _position_ (exclusive). 85 | 1. Set _result_ to the string-concatenation of _result_, _stringSlice_, and _replacement_. 86 | 1. Set _endOfLastMatch_ to _position_ + _searchLength_. 87 | 1. If _endOfLastMatch_ < the length of _string_, then 88 | 1. Set _result_ to the string-concatenation of _result_ and the substring of _string_ consisting of the code units from _endOfLastMatch_ (inclusive) up through the final code unit of _string_ (inclusive). 89 | 1. Return _result_. 90 | 91 |
92 | 93 | 94 | 95 |

String.prototype.matchAll ( _regexp_ )

96 |

Performs a regular expression match of the String representing the *this* value against _regexp_ and returns an iterator. Each iteration result's value is an Array object containing the results of the match, or *null* if the String did not match.

97 |

When the `matchAll` method is called, the following steps are taken:

98 | 99 | 1. Let _O_ be ? RequireObjectCoercible(*this* value). 100 | 1. If _regexp_ is neither *undefined* nor *null*, then 101 | 1. Let _isRegExp_ be ? IsRegExp(_regexp_). 102 | 1. If _isRegExp_ is *true*, then 103 | 1. Let _flags_ be ? Get(_regexp_, *"flags"*). 104 | 1. Perform ? RequireObjectCoercible(_flags_). 105 | 1. If ? ToString(_flags_) does not contain *"g"*, throw a *TypeError* exception. 106 | 1. Let _matcher_ be ? GetMethod(_regexp_, @@matchAll). 107 | 1. If _matcher_ is not *undefined*, then 108 | 1. Return ? Call(_matcher_, _regexp_, « _O_ »). 109 | 1. Let _S_ be ? ToString(_O_). 110 | 1. Let _rx_ be ? RegExpCreate(_regexp_, `"g"`). 111 | 1. Return ? Invoke(_rx_, @@matchAll, « _S_ »). 112 | 113 | The `matchAll` function is intentionally generic, it does not require that its *this* value be a String object. Therefore, it can be transferred to other kinds of objects for use as a method. 114 | Similarly to `String.prototype.split`, `String.prototype.matchAll` is designed to typically act without mutating its inputs. 115 |
116 | 117 | 118 |

String.prototype.indexOf ( _searchString_ [ , _position_ ] )

119 | 120 |

If _searchString_ appears as a substring of the result of converting this object to a String, at one or more indices that are greater than or equal to _position_, then the smallest such index is returned; otherwise, -1 is returned. If _position_ is *undefined*, 0 is assumed, so as to search all of the String.

121 |
122 |

The `indexOf` method takes two arguments, _searchString_ and _position_, and performs the following steps:

123 | 124 | 1. Let _O_ be ? RequireObjectCoercible(*this* value). 125 | 1. Let _S_ be ? ToString(_O_). 126 | 1. Let _searchStr_ be ? ToString(_searchString_). 127 | 1. Let _pos_ be ? ToInteger(_position_). 128 | 1. Assert: If _position_ is *undefined*, then _pos_ is 0. 129 | 1. Let _len_ be the length of _S_. 130 | 1. Let _start_ be min(max(_pos_, 0), _len_). 131 | 1. Let _searchLen_ be the length of _searchStr_. 132 | 1. Return the smallest possible integer _k_ not smaller than _start_ such that _k_ + _searchLen_ is not greater than _len_, and for all nonnegative integers _j_ less than _searchLen_, the code unit at index _k_ + _j_ within _S_ is the same as the code unit at index _j_ within _searchStr_; but if there is no such integer _k_, return the value -1. 133 | 1. Let _index_ be ! StringIndexOf(_S_, _searchStr_, _start_). 134 | 1. Return _index_. 135 | 136 | 137 |

The `indexOf` function is intentionally generic; it does not require that its *this* value be a String object. Therefore, it can be transferred to other kinds of objects for use as a method.

138 |
139 |
140 | --------------------------------------------------------------------------------