├── .eslintignore ├── .eslintrc.json ├── .gitattributes ├── .github └── workflows │ └── test.yml ├── .gitignore ├── .jsdoc.conf.js ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── rollup.config.js ├── scripts ├── lib │ └── tsdConvertTupleArrays.js └── tsd-postprocess.js ├── src ├── MatchingContext.js ├── ReplacementStringBuilder.js ├── UnionReplacer.js ├── UnionReplacerElement.js ├── typedefs-tscompat.js └── typedefs.js ├── test ├── .eslintrc.json ├── jasmine.json ├── markdown-doc-behavior.js ├── matching-context.spec.js ├── readme.spec.js ├── union-replacer-matching.spec.js └── union-replacer.spec.js └── types ├── index.d.ts ├── test.ts ├── tsconfig.json └── tslint.json /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["airbnb-base", "plugin:jsdoc/recommended"], 3 | "plugins": ["jsdoc"], 4 | "rules": { 5 | "no-plusplus": "off", 6 | "no-cond-assign": ["error", "except-parens"], 7 | "no-unused-expressions": ["error", { "allowShortCircuit": true }], 8 | "jsdoc/require-jsdoc": ["warn", { 9 | "publicOnly": true 10 | }], 11 | "jsdoc/no-undefined-types": ["warn", { 12 | "definedTypes": [ 13 | "UnionReplacer", 14 | "UnionReplacer.ReplacementBuilder", 15 | "RegExpExecArray", 16 | "true", 17 | "false", 18 | "T" 19 | ] 20 | }], 21 | "jsdoc/check-tag-names": ["warn", { 22 | "definedTags": ["template"] 23 | }], 24 | "jsdoc/check-examples": 1, 25 | "jsdoc/check-syntax": 1, 26 | "jsdoc/match-description": 1, 27 | "jsdoc/require-description": ["warn", { 28 | "exemptedBy": ["inheritdoc", "private", "deprecated", "hideconstructor"] 29 | }], 30 | "jsdoc/require-description-complete-sentence": 1, 31 | "jsdoc/require-param": ["warn", { 32 | "exemptedBy": ["inheritdoc", "hideconstructor"] 33 | }], 34 | "jsdoc/require-example": ["warn", { 35 | "contexts" : [ 36 | "ClassDeclaration[id.name='UnionReplacer'] > ClassBody > MethodDefinition" 37 | ], 38 | "exemptedBy": ["inheritdoc", "private", "deprecated"] 39 | }], 40 | "jsdoc/require-hyphen-before-param-description": 1 41 | }, 42 | "settings": { 43 | "jsdoc": { 44 | "mode": "jsdoc", 45 | "tagNamePreference": { 46 | "function": "method" 47 | } 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test-matrix 2 | on: push 3 | jobs: 4 | build: 5 | runs-on: ${{ matrix.os }} 6 | strategy: 7 | matrix: 8 | os: [ macos-latest, ubuntu-latest, windows-latest ] 9 | steps: 10 | - uses: actions/checkout@v1 11 | - uses: actions/setup-node@v1 12 | - run: npm install 13 | - run: npm run build --if-present 14 | - run: npm test 15 | env: 16 | CI: true 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | 63 | # Build directory 64 | dist 65 | -------------------------------------------------------------------------------- /.jsdoc.conf.js: -------------------------------------------------------------------------------- 1 | const template = env.opts.template || ''; 2 | const targetTs = template.includes('tsd-jsdoc'); 3 | 4 | const sourceIncludes = [ 5 | 'src/UnionReplacer.js', 6 | 'src/MatchingContext.js', 7 | 'src/typedefs.js', 8 | ]; 9 | 10 | if (!targetTs) { 11 | sourceIncludes.push('src/typedefs-tscompat.js'); 12 | } 13 | 14 | module.exports = { 15 | source: { include: sourceIncludes }, 16 | plugins: [ 17 | './node_modules/tsd-jsdoc/dist/plugin', 18 | 'plugins/markdown', 19 | ], 20 | opts: { 21 | package: 'package.json', 22 | readme: 'README.md', 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Orchitech Solutions, s.r.o. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # UnionReplacer 2 | 3 |
6 | 7 | UnionReplacer provides one-pass global search and replace functionality 8 | using multiple regular expressions and corresponging replacements. 9 | Otherwise the behavior matches `String.prototype.replace(regexp, newSubstr|function)`. 10 | 11 | ## Outline 12 | 13 | ### Installation and usage 14 | 15 | In browsers: 16 | ```html 17 | 18 | ``` 19 | 20 | Using [npm](https://www.npmjs.com/): 21 | ```bash 22 | npm install union-replacer 23 | ``` 24 | 25 | In [Node.js](http://nodejs.org/): 26 | ```js 27 | const UnionReplacer = require('union-replacer'); 28 | ``` 29 | 30 | With [TypeScript](https://www.typescriptlang.org/): 31 | ```js 32 | // with esModuleInterop enabled in tsconfig (recommended): 33 | import UnionReplacer from 'union-replacer'; 34 | // without esModuleInterop enabled in tsconfig: 35 | import * as UnionReplacer from 'union-replacer'; 36 | // regardless esModuleInterop setting: 37 | import UnionReplacer = require('union-replacer'); 38 | ``` 39 | 40 | ### Synopsis 41 | 42 | ``` 43 | replacer = new UnionReplacer(replace_pairs, [flags]) 44 | newStr = replacer.replace(str) 45 | ``` 46 | 47 | ### Parameters 48 | 49 | - `replace_pairs`: array of `[regexp, replacement]` arrays, where 50 | - `regexp`: particular regexp element in unioned regexp. Its eventual flags are ignored. 51 | - `replacement` corresponds with `String.prototype.replace`: 52 | - `function`: see 53 | [Specifying a function as a parameter](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace#Specifying_a_function_as_a_parameter). 54 | As of 1.1.0, the function can be called with extended arguments, see JSDoc 55 | for more info. 56 | - `newSubstr`: see 57 | [Specifying a string as a parameter](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace#Specifying_a_string_as_a_parameter). 58 | - `flags`: regular expression flags to be set on the main underlying regexp, defaults to `gm`. 59 | 60 | ### API updates 61 | 62 | - v2.0 removes the `addReplacement()` method, see 63 | [#4](https://github.com/orchitech/union-replacer/issues/4) for details. 64 | - v2.0 introduces TypeScript type definitions along with precise JSDoc type definitions. 65 | 66 | ## Examples 67 | 68 | ### Convenient one-pass escaping of HTML special chars 69 | ```js 70 | const htmlEscapes = [ 71 | [/, '<'], 72 | [/>/, '>'], 73 | [/"/, '"'], 74 | 75 | // not affected by the previous replacements producing '&' 76 | [/&/, '&'] 77 | ]; 78 | const htmlEscaper = new UnionReplacer(htmlEscapes); 79 | const toBeHtmlEscaped = ''; 80 | console.log(htmlEscaper.replace(toBeHtmlEscaped)); 81 | ``` 82 | Output: 83 | ``` 84 | <script>alert("inject & control")</script> 85 | ``` 86 | 87 | ### Simple Markdown highlighter 88 | 89 | Highlighting Markdown special characters while preserving code blocks and spans. 90 | Only a subset of Markdown syntax is supported for simplicity. 91 | ~~~js 92 | const mdHighlighter = new UnionReplacer([ 93 | 94 | // opening fence = at least three backticks 95 | // closing fence = opening fence or longer 96 | // regexp backreferences are ideal to match this 97 | [/^(`{3,}).*\n([\s\S]*?)(^\1`*\s*?$|\Z)/, (match, fence1, pre, fence2) => { 98 | let block = `${fence1}${htmlEscaper.replace(pre)}
${htmlEscaper.replace(code)}
`
110 | }],
111 |
112 | // Subsequent replaces are performed only outside code blocks and spans.
113 | [/[*~=+_-`]+/, '$&'],
114 | [/\n/, '_Markdown_ within fenced code blocks is not *processed*: 138 | ``` 139 | Even embedded "fence strings" work well with **UnionEscaper** 140 | ``` 141 |
${htmlEscaper.replace(pre)}
${myHtmlEscape.replace(code)}
`
243 | } else if (special) {
244 | return `${special}`
245 | } else if (nl) {
246 | return 'Create a UnionReplacer instance performing the specified replaces.
10 | * @example 11 | * replacer = new UnionReplacer([[/\$foo\b/, 'bar'], [/\\(.)/, '$1']]); 12 | * @example 13 | * // Simple URI encoder 14 | * replacer = new UnionReplacer([ 15 | * [/ /, '+'], 16 | * [/[^\w.,-]/, (m) => `%${m.charCodeAt(0).toString(16)}`], 17 | * ]); 18 | * @example 19 | * replacer = new UnionReplacer([[/\$foo\b/, 'bar'], [/\\(.)/, '$1']], 'gi'); 20 | * @param replaces -Replaces to be performed
21 | * specified as an array of {@link UnionReplacer.ReplaceTuple} array tuples.
22 | * The order of elements in replaces
is important: if any pattern is matched,
23 | * the corresponding amount of input is consumed and subsequent patterns
24 | * will not match on such part of the input.
Flags for replacement, defaults to 'gm'.
26 | */ 27 | declare class UnionReplacer { 28 | constructor(replaces: UnionReplacer.ReplaceTuple[], flags?: string); 29 | readonly flags: string; 30 | /** 31 | *Build the underlying combined regular expression. This method has no effect 32 | * since v2.0, as the builder-like functionality has been removed and underlying 33 | * data structures are prepared in the constructor.
34 | */ 35 | compile(): void; 36 | /** 37 | *Perform search and replace with the combined patterns and use corresponding 38 | * replacements for the particularly matched patterns.
39 | * @param subject -Input to search and process.
40 | * @param [userCtx = {}] -User-provided context to be passed as this
41 | * when calling replacement functions and as a parameter of the builder calls.
New string with the matches replaced. Or any type when a 43 | * custom builder is provided.
44 | */ 45 | replace(subject: string, userCtx?: any): string; 46 | /** 47 | *Perform search and replace with the combined patterns and use corresponding 48 | * replacements for the particularly matched patterns. Pass the resulting chunks 49 | * to an user-provided {@link UnionReplacer.ReplacementBuilder} instead of 50 | * concatenating them into one string.
51 | * @example 52 | * replacer.replace('foo'); 53 | * @param subject -Input to search and process.
54 | * @param userCtx -User-provided context to be passed as this
when
55 | * calling replacement functions and as a parameter of the builder calls.
Collects and builds 57 | * the result from unmatched subject slices and replaced matches. A custom 58 | * builder allows for creating arbitrary structures based on matching or 59 | * streaming these chunks without building any output.
60 | * @returnsResult built by the builder.
61 | */ 62 | replaceEncapsulation of matcher variables.
68 | */ 69 | interface MatchingContext { 70 | /** 71 | *The {@link UnionReplacer} instance being used.
72 | */ 73 | replacer: UnionReplacer; 74 | /** 75 | *Last match, as returned by {@link RegExp#exec}.
76 | */ 77 | match: RegExpExecArray | null; 78 | /** 79 | *Advance matching position n
characters after the match end position.
Number of characters to skip. Zero and negative values 81 | * are valid, but introduce risk of infinite processing. It is then user 82 | * responsibility to prevent it.
83 | */ 84 | skip(n: number): void; 85 | /** 86 | *Set matching position to n
characters from match start.
Number of characters jump. Values less than or equal 88 | * to match length are valid, but introduce risk of infinite processing. 89 | * It is then user responsibility to prevent it.
90 | */ 91 | jump(n: number): void; 92 | /** 93 | *Reset matching position according to standard regexp match position advancing.
94 | */ 95 | reset(): void; 96 | /** 97 | *Determine whether the current match is at the input start.
98 | * @returnstrue
if current match is at input start, false
otherwise.
Determine whether the current match is at the input end.
103 | * @returnstrue
if current match is at input end, false
otherwise.
Replacement callback function, as defined for String.prototype.replace
.
Extended replacement callback function that provides more options during processing.
113 | */ 114 | type ExtendedReplaceCb = (ctx: UnionReplacer.MatchingContext) => string; 115 | /** 116 | *Particular replace with ECMAScript string replacement.
117 | * @property 0 -Particular regexp to match.
118 | * @property 1 -Replacement string, as defined for String.prototype.replace
.
Particular replace with ECMAScript callback replacement.
123 | * @property 0 -Particular regexp to match.
124 | * @property 1 -Replacement callback, as defined for String.prototype.replace
.
Particular replace with extended callback replacement (UnionReplacer specific).
129 | * @property 0 -Particular regexp to match.
130 | * @property 1 -Replacement callback accepting 131 | * {@link UnionReplacer.MatchingContext}.
132 | * @property 2 -Flag true
marking the callback as {@link UnionReplacer.ExtendedReplaceCb}.
Particular replace with explicitly set ECMAScript callback replacement. 137 | * Leads to the same behavior as {@link UnionReplacer.ReplaceWithCb}.
138 | * @property 0 -Particular regexp to match.
139 | * @property 1 -Replacement callback accepting 140 | * {@link UnionReplacer.MatchingContext}.
141 | * @property 2 -Flag false
marking the callback as {@link UnionReplacer.StringReplaceCb}.
Particular replace definition similiar to {@link String#replace} arguments specified 146 | * as an array (tuple) with the following items:
147 | *$<name>
.
155 | * Replacement function is by default the {@link String#replace}-style callback:(match, p1, ..., pn, offset, string, namedCaptures) => { ... }
.
161 | * Unlike numbered captures that are narrowed for the particular match,
162 | * this extra namedCaptures
parameter would contain keys for all the named
163 | * capture groups within the replacer and the values of "foreign" named captures
164 | * would be always undefined
.
165 | * Replacement callback can also be specified as extended
. Then only one
166 | * parameter is passed, an instance of {@link UnionReplacer.MatchingContext}.
167 | * This variant is more powerful.extended
flag - if true, the {@link UnionReplacer.MatchingContext}
171 | * will be passed to the replacement function instead of {@link String#replace}-ish
172 | * parameters.Interface for processors of string chunks during replacement process.
178 | */ 179 | interface ReplacementBuilderProcess unmatched slice of the input string.
182 | * @example 183 | * builder.addSubjectSlice('example', 1, 2); 184 | * @param subject -String to be processed.
185 | * @param start -Zero-based index at which to begin extraction.
186 | * @param end -Zero-based index before which to end extraction. 187 | * The character at this index will not be included.
188 | */ 189 | addSubjectSlice(subject: string, start: number, end: number): void; 190 | /** 191 | *Process replaced match.
192 | * @example 193 | * builder.addReplacedString('example'); 194 | * @param string -String to be processed.
195 | */ 196 | addReplacedString(string: string): void; 197 | /** 198 | *Build output to be returned by {@link UnionReplacer#replace(2)}.
199 | * @example 200 | * const x = builder.build(); 201 | * @returnsOutput to be returned by {@link UnionReplacer#replace(2)}.
202 | */ 203 | build(): T; 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /types/test.ts: -------------------------------------------------------------------------------- 1 | import UnionReplacer = require('union-replacer'); 2 | 3 | new UnionReplacer([]); 4 | new UnionReplacer(); // $ExpectError 5 | new UnionReplacer([/foo/, 'bar']); // $ExpectError 6 | new UnionReplacer([['foo', 'bar']]); // $ExpectError 7 | 8 | new UnionReplacer([[/foo/, 'bar']]); 9 | new UnionReplacer([[/foo/, 'bar', true]]); // $ExpectError 10 | new UnionReplacer([[/foo/, 'bar', false]]); // $ExpectError 11 | 12 | new UnionReplacer([[/foo/, (m: string, index: number): string => '']]); 13 | new UnionReplacer([[/foo/, (m: string, index: number): number => 1]]); // $ExpectError 14 | new UnionReplacer([[/foo/, (m: string): string => m, false]]); 15 | new UnionReplacer([[/foo/, (m: string): string => m, true]]); // $ExpectError 16 | 17 | new UnionReplacer([[/foo/, (ctx: UnionReplacer.MatchingContext): string => '', true]]); 18 | new UnionReplacer([[/foo/, (ctx: UnionReplacer.MatchingContext): number => 1, true]]); // $ExpectError 19 | new UnionReplacer([[/foo/, (ctx: UnionReplacer.MatchingContext): string => '', false]]); // $ExpectError 20 | 21 | const replacer: UnionReplacer = new UnionReplacer([ 22 | [/foo/, (m, index) => { 23 | m; // $ExpectType string 24 | return ''; 25 | }], 26 | [/bar/, (ctx: UnionReplacer.MatchingContext) => { 27 | ctx; // $ExpectType MatchingContext 28 | ctx.match; // $ExpectType RegExpExecArray | null 29 | return ''; 30 | }, true], 31 | ]); 32 | 33 | replacer.replace('foobar'); // $ExpectType string 34 | 35 | class MyBuilder implements UnionReplacer.ReplacementBuilder