diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..1320b9a --- /dev/null +++ b/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["@babel/preset-env"] +} diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index 311c21f..527c323 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -3,7 +3,7 @@ name: Node Test on: push: branches: - - master + - "*" pull_request: branches: - master @@ -40,5 +40,4 @@ jobs: run: yarn - name: Test run: | - yarn test - yarn test-with-coverage \ No newline at end of file + yarn test \ No newline at end of file diff --git a/.gitignore b/.gitignore index e881464..22e3994 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,8 @@ yarn.lock package-lock.json # test -.nyc_output \ No newline at end of file +.nyc_output +.test_output + +# build +dist diff --git a/README.md b/README.md index 51c99bc..4b538b1 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # string-multiple-replace -> Replace multiple substrings in a string in turn. +> Replace multiple substrings in a string sequentially. [![LICENSE](https://img.shields.io/badge/license-MIT-blue)](./LICENSE) [![npm-version](https://img.shields.io/npm/v/string-multiple-replace)](https://www.npmjs.com/package/string-multiple-replace) @@ -71,7 +71,7 @@ multiReplace(input, matcherObj, keys => keys); ## API -> multiReplace(input, matcherObj[,sequencer]) +> multiReplace(input, matcherObj [,sequencer]) The original string is replaced in turn according to the `matcherObj`, where `sequencer` determines the replacement order, and the existence state of `sequencer` determines whether the last operation overwrites the previous operation. @@ -99,4 +99,4 @@ Type: `function`, `array` A `function` that takes the keys of `matcherObj`, and return an suquence array. ->Upgrade Instruction: the existence state of `sequencer` determines whether the last operation overwrites the previous operation. +> Upgrade Instruction: the existence state of `sequencer` determines whether the last operation overwrites the previous operation. diff --git a/commitlint.config.js b/commitlint.config.cjs similarity index 100% rename from commitlint.config.js rename to commitlint.config.cjs diff --git a/package.json b/package.json index d07a3e3..1a6c88a 100644 --- a/package.json +++ b/package.json @@ -1,25 +1,42 @@ { "name": "string-multiple-replace", - "version": "1.0.5", - "description": "Replace multiple substrings in a string in turn", + "version": "1.1.0", + "description": "Replace multiple substrings in a string sequentially", "author": "iChengbo", "scripts": { + "build": "rollup -c", "test": "mocha", - "test-with-coverage": "nyc --reporter=text mocha", + "test:coverage": "nyc mocha test/**/*.js", "prepare": "husky install" }, - "main": "index.js", - "directories": { - "lib": "lib", - "test": "test" + "type": "module", + "main": "dist/index.cjs.js", + "module": "dist/index.esm.js", + "exports": { + ".": { + "require": "./dist/index.cjs.js", + "import": "./dist/index.esm.js" + } }, + "files": [ + "dist" + ], "devDependencies": { - "@commitlint/cli": "^17.1.2", - "@commitlint/config-conventional": "^17.1.0", - "chai": "^4.3.4", - "husky": "^8.0.3", - "mocha": "^10.2.0", - "nyc": "^15.1.0" + "@babel/core": "^7.24.8", + "@babel/preset-env": "^7.24.8", + "@babel/register": "^7.24.6", + "@commitlint/cli": "^19.3.0", + "@commitlint/config-conventional": "^19.2.2", + "@rollup/plugin-commonjs": "^26.0.1", + "@rollup/plugin-node-resolve": "^15.2.3", + "@rollup/plugin-terser": "^0.4.4", + "babel-preset-env": "^1.7.0", + "babel-register": "^6.26.0", + "chai": "^5.1.1", + "husky": "^9.0.11", + "mocha": "^10.6.0", + "nyc": "^17.0.0", + "rollup": "^4.18.1" }, "keywords": [ "replace", diff --git a/rollup.config.js b/rollup.config.js new file mode 100644 index 0000000..d9259f3 --- /dev/null +++ b/rollup.config.js @@ -0,0 +1,27 @@ +import { nodeResolve } from "@rollup/plugin-node-resolve"; +import commonjs from "@rollup/plugin-commonjs"; +import terser from "@rollup/plugin-terser"; + +export default { + input: "src/index.js", + output: [ + { + file: "dist/index.esm.js", + format: "es", + }, + { + file: "dist/index.cjs.js", + format: "cjs", + }, + { + file: "dist/index.umd.js", + format: "umd", + name: 'StringMultipleReplace' + } + ], + plugins: [ + nodeResolve(), + commonjs(), + terser() + ], +}; diff --git a/src/core/replace-with-cover.js b/src/core/replace-with-cover.js new file mode 100644 index 0000000..88eb454 --- /dev/null +++ b/src/core/replace-with-cover.js @@ -0,0 +1,20 @@ +const replaceAll = (replaceThis, withThis, inThis) => inThis.split(replaceThis).join(withThis); + +const isObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value); + +const stringifyValue = (value) => isObject(value) ? JSON.stringify(value) : String(value); + +const getSequence = (keys, matcherObj) => typeof keys === "function" ? keys(Object.keys(matcherObj)) : keys; + +const replaceWithCover = (input, matcherObj, sequence) => { + let output = input; + const sequencer = getSequence(sequence, matcherObj); + for (let i = 0; i < sequencer.length; i++) { + const key = sequencer[i]; + const withThis = matcherObj[key] !== undefined ? stringifyValue(matcherObj[key]) : key; + output = replaceAll(key, withThis, output); + } + return output; +}; + +export default replaceWithCover; diff --git a/src/core/replace-without-cover.js b/src/core/replace-without-cover.js new file mode 100644 index 0000000..77d25c5 --- /dev/null +++ b/src/core/replace-without-cover.js @@ -0,0 +1,45 @@ +const replaceRange = (input, items) => { + items.sort((a, b) => a[0] - b[0]); + + let result = ""; + let index = 0; + items.forEach((item) => { + const [start, end, callback] = item; + result += input.substring(index, start); + result += callback(input.substring(start, end)); + index = end; + }); + + result += input.substring(index); + + return result; +}; + +const generateItemByTarget = (input, target, replaced) => { + const items = []; + let index = 0; + while (true) { + const idx = input.indexOf(target, index); + if (idx === -1) { + break; + } + items.push([idx, idx + target.length, () => replaced]); + index = idx + 1; + } + return items; +}; + +const generateItems = (input, matcherObj) => { + return Object.keys(matcherObj).reduce((items, key) => { + const generatedItems = generateItemByTarget(input, key, matcherObj[key]); + return items.concat(generatedItems); + }, []); +}; + +// this replacement operation will not be affected next time. +const replaceWithoutCover = (input, matcherObj) => { + const items = generateItems(input, matcherObj); + return replaceRange(input, items); +}; + +export default replaceWithoutCover; diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..4cd8ae8 --- /dev/null +++ b/src/index.js @@ -0,0 +1,55 @@ +import replaceWithCover from "./core/replace-with-cover.js"; +import replaceWithoutCover from "./core/replace-without-cover.js"; + +const isSubArray = (arr, subArr) => subArr.every((s) => arr.includes(s)); + +/** + * @param {String} input A string to be processed. + * @param {Object} matcherObj An object that represents a string replacement mapping. + * @param {Array|Function} sequencer A `function` that takes the keys of `matcherObj`, and return an suquence array. + */ +const multiReplace = (...args) => { + const [input, matcherObj, sequencer] = args + if (args.length !== 2 && args.length !== 3) { + throw new TypeError("The number of parameters is incorrect."); + } + + if (typeof input !== "string") { + throw new TypeError(`Expected input to be a string, got ${typeof input}`); + } + + if (typeof matcherObj !== "object") { + throw new TypeError( + `Expected matcherObj to be a object, got ${typeof matcherObj}` + ); + } + + if (Object.keys(matcherObj).length === 0) { + return input; + } + + if (sequencer) { + if (typeof sequencer !== "function" && !Array.isArray(sequencer)) { + throw new TypeError( + `Expected sequencer to be a callback or array, got ${Object.prototype.toString.call( + sequencer + )}` + ); + } + + const keys = Object.keys(matcherObj); + const sequence = Array.isArray(sequencer) ? sequencer : sequencer(keys); + + if (!isSubArray(Object.keys(matcherObj), sequence)) { + throw new TypeError( + `Expected sequence is the subset of Object.keys(matcherObj), got: ${sequence}` + ); + } + + return replaceWithCover(input, matcherObj, sequence); + } else { + return replaceWithoutCover(input, matcherObj); + } +}; + +export default multiReplace; diff --git a/test/index.js b/test/index.js index ed30805..890dfd4 100644 --- a/test/index.js +++ b/test/index.js @@ -1,8 +1,8 @@ -const should = require('chai').should(); -const expect = require('chai').expect; -const multiReplace = require('../index'); +import { expect } from 'chai'; +// import { default as multiReplace } from '../dist/index.esm.js' +import multiReplace from '../src/index.js' -describe('String-multiple-replace', function () { +describe('String-Multiple-Replace', function () { describe("need cover previous replacement", function () { it('Replace brave & trouble from a text with cowardly & escape, and sequencer is a array that decide the order of replacement.', function () { @@ -13,8 +13,8 @@ describe('String-multiple-replace', function () { } const sequencer = ["brave", "trouble"]; - const resultStr = multiReplace(input, matcherObj, sequencer);; - resultStr.should.equal("I'm only cowardly when I have to be. Being cowardly doesn't mean you go looking for escape."); + const resultStr = multiReplace(input, matcherObj, sequencer); + expect(resultStr).to.equal("I'm only cowardly when I have to be. Being cowardly doesn't mean you go looking for escape."); }); it('Replace brave & trouble from a text with cowardly & escape, and sequencer is a function that will return a array', function () { @@ -27,7 +27,7 @@ describe('String-multiple-replace', function () { const resultStr = multiReplace(input, matcherObj, function (keys) { return keys; //or keys.sort(callback) }); - resultStr.should.equal("I'm only cowardly when I have to be. Being cowardly doesn't mean you go looking for escape."); + expect(resultStr).to.equal("I'm only cowardly when I have to be. Being cowardly doesn't mean you go looking for escape."); }); it("Replace 'My friend' with 'I', but then 'I' will be overwritten with 'My friend'", function () { @@ -40,12 +40,12 @@ describe('String-multiple-replace', function () { const sequencer = ["My friend", "has", "I"]; const resultStr = multiReplace(input, matcherObj, sequencer);; - resultStr.should.equal("My friend have a dog. My friend want a dog too!"); + expect(resultStr).to.equal("My friend have a dog. My friend want a dog too!"); }); }); describe("needn't cover previous replacement", function () { - it("Replace 'My firend' with 'I', and then 'I' will not be overwritten with 'My friend'", function () { + it("Replace 'My friend' with 'I', and then 'I' will not be overwritten with 'My friend'", function () { const input = "My friend has a dog. I want a dog too!"; const matcherObj = { "My friend": "I", @@ -54,7 +54,7 @@ describe('String-multiple-replace', function () { } const resultStr = multiReplace(input, matcherObj);; - resultStr.should.equal("I have a dog. My friend want a dog too!"); + expect(resultStr).to.equal("I have a dog. My friend want a dog too!"); }); it("Replace all 'a' with 'b', and replace all 'b' with 'a'", function () { @@ -65,12 +65,12 @@ describe('String-multiple-replace', function () { } const resultStr = multiReplace(input, matcherObj); - resultStr.should.equal("bacd, bacd, b"); + expect(resultStr).to.equal("bacd, bacd, b"); }); }); describe("Some special cases", function () { - it('Replace "My firend" with "{"Name":"Tom"}"', function () { + it('Replace "My friend" with "{"Name":"Tom"}"', function () { const input = 'My friend has a dog. I want a dog too!'; const matcherObj = { 'My friend': { @@ -79,15 +79,15 @@ describe('String-multiple-replace', function () { } const sequencer = ["My friend"]; - const resultStr = multiReplace(input, matcherObj, sequencer);; - resultStr.should.equal('{"Name":"Tom"} has a dog. I want a dog too!'); + const resultStr = multiReplace(input, matcherObj, sequencer); + expect(resultStr).to.equal('{"Name":"Tom"} has a dog. I want a dog too!'); }); it("should return 'input'", function () { const input = 'My friend has a dog. I want a dog too!'; const matcherObj = {} const resultStr = multiReplace(input, matcherObj);; - resultStr.should.equal('My friend has a dog. I want a dog too!'); + expect(resultStr).to.equal('My friend has a dog. I want a dog too!'); }); });