├── .editorconfig ├── .gitattributes ├── .github └── workflows │ └── main.yml ├── .gitignore ├── .npmrc ├── index.d.ts ├── index.js ├── index.test-d.ts ├── license ├── package.json ├── readme.md └── test.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.yml] 11 | indent_style = space 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | - push 4 | - pull_request 5 | jobs: 6 | test: 7 | name: Node.js ${{ matrix.node-version }} 8 | runs-on: ubuntu-latest 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | node-version: 13 | - 16 14 | steps: 15 | - uses: actions/checkout@v2 16 | - uses: actions/setup-node@v2 17 | with: 18 | node-version: ${{ matrix.node-version }} 19 | - run: npm install 20 | - run: npm test 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | yarn.lock 3 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | export type ReplacementFunction = ( 2 | matchedSubstring: string, 3 | matchCount: number, 4 | input: string, 5 | matchIndex: number 6 | ) => string; 7 | 8 | export interface Options { 9 | /** 10 | Index at which to start replacing. 11 | 12 | @default 0 13 | */ 14 | readonly fromIndex?: number; 15 | 16 | /** 17 | Whether or not substring matching should be case-insensitive. 18 | 19 | @default false 20 | */ 21 | readonly caseInsensitive?: boolean; 22 | } 23 | 24 | /** 25 | Replace all substring matches in a string. 26 | 27 | @param input - The string to work on. 28 | @param needle - The string to match in `input`. 29 | @param replacement - The replacement for `needle` matches. 30 | @returns A new string with all `needle` matches replaced with `replacement`. 31 | 32 | @example 33 | ``` 34 | import replaceString from 'replace-string'; 35 | 36 | const string = 'My friend has a 🐑. I want a 🐑 too!'; 37 | 38 | replaceString(string, '🐑', '🦄'); 39 | //=> 'My friend has a 🦄. I want a 🦄 too!' 40 | 41 | replaceString('Foo 🐑 Bar', '🐑', (matchedSubstring, matchCount, input, matchIndex) => `${matchedSubstring}❤️`); 42 | //=> 'Foo 🐑❤️ Bar' 43 | ``` 44 | */ 45 | export default function replaceString( 46 | input: string, 47 | needle: string, 48 | replacement: string | ReplacementFunction, 49 | options?: Options 50 | ): string; 51 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | export default function replaceString(string, needle, replacement, options = {}) { 2 | if (typeof string !== 'string') { 3 | throw new TypeError(`Expected input to be a string, got ${typeof string}`); 4 | } 5 | 6 | if ( 7 | !(typeof needle === 'string' && needle.length > 0) 8 | || !(typeof replacement === 'string' || typeof replacement === 'function') 9 | ) { 10 | return string; 11 | } 12 | 13 | let result = ''; 14 | let matchCount = 0; 15 | let previousIndex = options.fromIndex > 0 ? options.fromIndex : 0; 16 | 17 | if (previousIndex > string.length) { 18 | return string; 19 | } 20 | 21 | while (true) { // eslint-disable-line no-constant-condition 22 | const index = options.caseInsensitive 23 | ? string.toLowerCase().indexOf(needle.toLowerCase(), previousIndex) 24 | : string.indexOf(needle, previousIndex); 25 | 26 | if (index === -1) { 27 | break; 28 | } 29 | 30 | matchCount++; 31 | 32 | const replaceString_ = typeof replacement === 'string' ? replacement : replacement( 33 | // If `caseInsensitive`` is enabled, the matched substring may be different from the needle. 34 | string.slice(index, index + needle.length), 35 | matchCount, 36 | string, 37 | index, 38 | ); 39 | 40 | // Get the initial part of the string on the first iteration. 41 | const beginSlice = matchCount === 1 ? 0 : previousIndex; 42 | 43 | result += string.slice(beginSlice, index) + replaceString_; 44 | 45 | previousIndex = index + needle.length; 46 | } 47 | 48 | if (matchCount === 0) { 49 | return string; 50 | } 51 | 52 | return result + string.slice(previousIndex); 53 | } 54 | -------------------------------------------------------------------------------- /index.test-d.ts: -------------------------------------------------------------------------------- 1 | import {expectType} from 'tsd'; 2 | import replaceString from './index.js'; 3 | 4 | const input = 'My friend has a 🐑. I want a 🐑 too!'; 5 | 6 | expectType(replaceString(input, '🐑', '🦄')); 7 | expectType( 8 | replaceString(input, '🐑', (needle, matchCount, input, matchIndex) => { 9 | expectType(needle); 10 | expectType(matchCount); 11 | expectType(input); 12 | expectType(matchIndex); 13 | 14 | return '🦄'; 15 | }), 16 | ); 17 | expectType(replaceString(input, '🐑', '🦄', {fromIndex: 1})); 18 | expectType(replaceString(input, '🐑', '🦄', {caseInsensitive: true as boolean})); 19 | expectType(replaceString(input, '🐑', '🦄', {fromIndex: 1, caseInsensitive: true as boolean})); 20 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Sindre Sorhus (https://sindresorhus.com) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "replace-string", 3 | "version": "4.0.0", 4 | "description": "Replace all substring matches in a string", 5 | "license": "MIT", 6 | "repository": "sindresorhus/replace-string", 7 | "funding": "https://github.com/sponsors/sindresorhus", 8 | "author": { 9 | "name": "Sindre Sorhus", 10 | "email": "sindresorhus@gmail.com", 11 | "url": "https://sindresorhus.com" 12 | }, 13 | "type": "module", 14 | "exports": "./index.js", 15 | "engines": { 16 | "node": "^12.20.0 || ^14.13.1 || >=16.0.0" 17 | }, 18 | "scripts": { 19 | "test": "xo && ava && tsd" 20 | }, 21 | "files": [ 22 | "index.js", 23 | "index.d.ts" 24 | ], 25 | "keywords": [ 26 | "replace", 27 | "string", 28 | "text", 29 | "all", 30 | "many", 31 | "multiple", 32 | "global", 33 | "match", 34 | "matches", 35 | "replacement", 36 | "replacer", 37 | "modify", 38 | "substring", 39 | "sub-string", 40 | "needle", 41 | "search", 42 | "replaceall" 43 | ], 44 | "devDependencies": { 45 | "ava": "^3.15.0", 46 | "tsd": "^0.17.0", 47 | "xo": "^0.44.0" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # replace-string 2 | 3 | > Replace all substring matches in a string 4 | 5 | Similar to `String#replace()`, but supports replacing multiple matches. You could achieve something similar by putting the string in a `RegExp` constructor with the global flag and passing it to `String#replace()`, but you would then have to first escape the string anyways. 6 | 7 | *With [Node.js 16](https://medium.com/@nodejs/node-js-v15-0-0-is-here-deb00750f278), this package is partly moot as there is now a [`String#replaceAll`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replaceAll) method. However, it does not have a `caseInsensitive` option.* 8 | 9 | ## Install 10 | 11 | ``` 12 | $ npm install replace-string 13 | ``` 14 | 15 | ## Usage 16 | 17 | ```js 18 | import replaceString from 'replace-string'; 19 | 20 | const string = 'My friend has a 🐑. I want a 🐑 too!'; 21 | 22 | replaceString(string, '🐑', '🦄'); 23 | //=> 'My friend has a 🦄. I want a 🦄 too!' 24 | ``` 25 | 26 | ## API 27 | 28 | ### replaceString(string, needle, replacement, options?) 29 | 30 | Returns a new string with all `needle` matches replaced with `replacement`. 31 | 32 | #### string 33 | 34 | Type: `string` 35 | 36 | The string to work on. 37 | 38 | #### needle 39 | 40 | Type: `string` 41 | 42 | The string to match in `input`. 43 | 44 | #### replacement 45 | 46 | Type: `string | Function` 47 | 48 | The replacement for `needle` matches. 49 | 50 | If a function, it receives the matched substring, the match count, the original input, and the index in which the match happened (as measured from the original input): 51 | 52 | ```js 53 | import replaceString from 'replace-string'; 54 | 55 | replaceString('Foo 🐑 Bar', '🐑', (matchedSubstring, matchCount, input, matchIndex) => `${matchedSubstring}❤️`); 56 | //=> 'Foo 🐑❤️ Bar' 57 | ``` 58 | 59 | #### options 60 | 61 | Type: `object` 62 | 63 | ##### fromIndex 64 | 65 | Type: `number`\ 66 | Default: `0` 67 | 68 | Index at which to start replacing. 69 | 70 | ##### caseInsensitive 71 | 72 | Type: `boolean`\ 73 | Default: `false` 74 | 75 | Whether or not substring matching should be case-insensitive. 76 | 77 | ## Related 78 | 79 | - [execall](https://github.com/sindresorhus/execall) - Find multiple `RegExp` matches in a string 80 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import replaceString from './index.js'; 3 | 4 | test('main', t => { 5 | t.is(replaceString('foo bar foo', 'bar', 'foo'), 'foo foo foo'); 6 | t.is(replaceString('', 'bar', 'foo'), ''); 7 | t.is(replaceString('foo', '', 'foo'), 'foo'); 8 | t.is(replaceString('foo', 'bar', ''), 'foo'); 9 | t.is(replaceString('foo'), 'foo'); 10 | t.is(replaceString('foo', 'bar'), 'foo'); 11 | t.is(replaceString('foo', 3, 3), 'foo'); 12 | 13 | t.is( 14 | replaceString('My friend has a 🐑. I want a 🐑 too!', '🐑', '🦄'), 15 | 'My friend has a 🦄. I want a 🦄 too!', 16 | ); 17 | 18 | t.is( 19 | replaceString('foo bar baz foo baz', 'foo', '🦄'), 20 | '🦄 bar baz 🦄 baz', 21 | ); 22 | 23 | t.is( 24 | replaceString('foo bar baz foo baz', 'foo', '🦄', {fromIndex: 5}), 25 | 'foo bar baz 🦄 baz', 26 | ); 27 | t.is(replaceString('foo', 3, 3, {fromIndex: 100}), 'foo'); 28 | t.is(replaceString('foo', 'foo', 'bar', {fromIndex: -100}), 'bar'); 29 | t.is(replaceString('foo foo foo foo foo', 'foo', 'bar', {fromIndex: 1}), 'foo bar bar bar bar'); 30 | t.is(replaceString('bar foo', 'foo', 'bar', {fromIndex: 5}), 'bar foo'); 31 | 32 | t.is(replaceString('foo bar foo', 'Bar', 'Foo', {caseInsensitive: true}), 'foo Foo foo'); 33 | }); 34 | 35 | test('function replacement', t => { 36 | const needle = 'foo'; 37 | const countIndices = []; 38 | const matchIndices = []; 39 | 40 | t.is( 41 | replaceString('foo bar baz foo baz', needle, (matchedSubstring, count, input, matchIndex) => { 42 | t.is(matchedSubstring, needle); 43 | countIndices.push(count); 44 | matchIndices.push(matchIndex); 45 | t.is(typeof input, 'string'); 46 | return `${matchedSubstring}2`; 47 | }), 48 | 'foo2 bar baz foo2 baz', 49 | ); 50 | 51 | t.deepEqual(countIndices, [1, 2]); 52 | t.deepEqual(matchIndices, [0, 12]); 53 | }); 54 | 55 | test('function replacement with `fromIndex` option', t => { 56 | const needle = 'foo'; 57 | const countIndices = []; 58 | const matchIndices = []; 59 | 60 | t.is( 61 | replaceString('foo bar baz foo baz Foo', needle, (matchedSubstring, count, input, matchIndex) => { 62 | t.is(matchedSubstring, needle); 63 | countIndices.push(count); 64 | matchIndices.push(matchIndex); 65 | t.is(typeof input, 'string'); 66 | return `${matchedSubstring}2`; 67 | }, {fromIndex: 5}), 68 | 'foo bar baz foo2 baz Foo', 69 | ); 70 | 71 | t.deepEqual(countIndices, [1]); 72 | t.deepEqual(matchIndices, [12]); 73 | }); 74 | 75 | test('function replacement with `fromIndex` and `caseInsensitive` options', t => { 76 | const needle = 'fOO'; 77 | const countIndices = []; 78 | const matchIndices = []; 79 | 80 | t.is( 81 | replaceString('fOO bar baz foo baz foo Foo fOo FoO', needle, (matchedSubstring, count, input, matchIndex) => { 82 | t.is(matchedSubstring.toLowerCase(), needle.toLowerCase()); 83 | countIndices.push(count); 84 | matchIndices.push(matchIndex); 85 | t.is(typeof input, 'string'); 86 | return `${matchedSubstring}2`; 87 | }, {fromIndex: 15, caseInsensitive: true}), 88 | 'fOO bar baz foo baz foo2 Foo2 fOo2 FoO2', 89 | ); 90 | 91 | t.deepEqual(countIndices, [1, 2, 3, 4]); 92 | t.deepEqual(matchIndices, [20, 24, 28, 32]); // Indexes are measured based on the original string 93 | }); 94 | --------------------------------------------------------------------------------