diff --git a/README.md b/README.md index d57629b..1b63f34 100644 --- a/README.md +++ b/README.md @@ -165,9 +165,9 @@ Same as `s.appendLeft(...)`, except that the inserted content will go *before* a Same as `s.appendRight(...)`, except that the inserted content will go *before* any previous appends or prepends at `index` -### s.replace( regexp, substitution ) +### s.replace( regexpOrString, substitution ) -String replacement with RegExp or string, a replacer function is also supported. Returns `this`. +String replacement with RegExp or string. When using a RegExp, replacer function is also supported. Returns `this`. ```ts import MagicString from 'magic-string' @@ -183,6 +183,11 @@ The differences from [`String.replace`]((https://developer.mozilla.org/en-US/doc - It will always match against the **original string** - It mutates the magic string state (use `.clone()` to be immutable) +### s.replaceAll( regexpOrString, substitution ) + +Same as `s.replace`, but replace all matched strings instead of just one. +If `substitution` is a regex, then it must have the global (`g`) flag set, or a `TypeError` is thrown. Matches the behavior of the bultin [`String.property.replaceAll`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replaceAll). + ### s.remove( start, end ) Removes the characters from `start` to `end` (of the original string, **not** the generated string). Removing the same content twice, or making removals that partially overlap, will cause an error. Returns `this`. diff --git a/src/MagicString.js b/src/MagicString.js index e45ad2e..2a6fe77 100644 --- a/src/MagicString.js +++ b/src/MagicString.js @@ -736,7 +736,7 @@ export default class MagicString { return this.original !== this.toString(); } - replace(searchValue, replacement) { + _replaceRegexp(searchValue, replacement) { function getReplacement(match, str) { if (typeof replacement === 'string') { return replacement.replace(/\$(\$|&|\d+)/g, (_, i) => { @@ -759,7 +759,7 @@ export default class MagicString { } return matches; } - if (typeof searchValue !== 'string' && searchValue.global) { + if (searchValue.global) { const matches = matchAll(searchValue, this.original); matches.forEach((match) => { if (match.index != null) @@ -780,4 +780,51 @@ export default class MagicString { } return this; } + + _replaceString(string, replacement) { + const { original } = this; + const index = original.indexOf(string); + + if (index !== -1) { + this.overwrite(index, index + string.length, replacement); + } + + return this; + } + + replace(searchValue, replacement) { + if (typeof searchValue === 'string') { + return this._replaceString(searchValue, replacement); + } + + return this._replaceRegexp(searchValue, replacement); + } + + _replaceAllString(string, replacement) { + const { original } = this; + const stringLength = string.length; + for ( + let index = original.indexOf(string); + index !== -1; + index = original.indexOf(string, index + stringLength) + ) { + this.overwrite(index, index + stringLength, replacement); + } + + return this; + } + + replaceAll(searchValue, replacement) { + if (typeof searchValue === 'string') { + return this._replaceAllString(searchValue, replacement); + } + + if (!searchValue.global) { + throw new TypeError( + 'MagicString.prototype.replaceAll called with a non-global RegExp argument' + ); + } + + return this._replaceRegexp(searchValue, replacement); + } } diff --git a/test/MagicString.js b/test/MagicString.js index 4bc1d09..c2b9c5b 100644 --- a/test/MagicString.js +++ b/test/MagicString.js @@ -1308,6 +1308,27 @@ describe('MagicString', () => { assert.strictEqual(s.toString(), '1 3 1 2'); }); + it('Should not treat string as regexp', () => { + assert.strictEqual( + new MagicString('1234').replace('.', '*').toString(), + '1234' + ); + }); + + it('Should use substitution directly', () => { + assert.strictEqual( + new MagicString('11').replace('1', '$0$1').toString(), + '$0$11' + ); + }); + + it('Should not search back', () => { + assert.strictEqual( + new MagicString('122121').replace('12', '21').toString(), + '212121' + ); + }); + it('works with global regex replace', () => { const s = new MagicString('1 2 3 4 a b c'); @@ -1347,4 +1368,61 @@ describe('MagicString', () => { ); }); }); + + describe('replaceAll', () => { + it('works with string replace', () => { + assert.strictEqual( + new MagicString('1212').replaceAll('2', '3').toString(), + '1313', + ); + }); + + it('Should not treat string as regexp', () => { + assert.strictEqual( + new MagicString('1234').replaceAll('.', '*').toString(), + '1234' + ); + }); + + it('Should use substitution directly', () => { + assert.strictEqual( + new MagicString('11').replaceAll('1', '$0$1').toString(), + '$0$1$0$1' + ); + }); + + it('Should not search back', () => { + assert.strictEqual( + new MagicString('121212').replaceAll('12', '21').toString(), + '212121' + ); + }); + + it('global regex result the same as .replace', () => { + assert.strictEqual( + new MagicString('1 2 3 4 a b c').replaceAll(/(\d)/g, 'xx$1$10').toString(), + new MagicString('1 2 3 4 a b c').replace(/(\d)/g, 'xx$1$10').toString(), + ); + + assert.strictEqual( + new MagicString('1 2 3 4 a b c').replaceAll(/(\d)/g, '$$').toString(), + new MagicString('1 2 3 4 a b c').replace(/(\d)/g, '$$').toString(), + ); + + assert.strictEqual( + new MagicString('hey this is magic').replaceAll(/(\w)(\w+)/g, (_, $1, $2) => `${$1.toUpperCase()}${$2}`).toString(), + new MagicString('hey this is magic').replace(/(\w)(\w+)/g, (_, $1, $2) => `${$1.toUpperCase()}${$2}`).toString(), + ); + }); + + it('rejects with non-global regexp', () => { + assert.throws( + () => new MagicString('123').replaceAll(/./, ''), + { + name: 'TypeError', + message: 'MagicString.prototype.replaceAll called with a non-global RegExp argument', + }, + ); + }); + }); });