├── .travis.yml ├── test ├── example-text │ ├── brackets-comments2.txt │ ├── brackets-unbalanced3.txt │ ├── brackets-mixed.txt │ ├── brackets-unbalanced.txt │ ├── brackets-unbalanced2.txt │ ├── brackets-unbalanced4.txt │ ├── brackets-head.txt │ ├── brackets-comments.txt │ ├── brackets-unbalanced5.txt │ └── brackets-basic.txt ├── balance.test.js ├── matches.test.js └── replacements.test.js ├── package.json ├── Gruntfile.js ├── dist ├── balanced-min.js ├── balanced.js └── balanced.js.map ├── README.md └── index.js /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 0.10 -------------------------------------------------------------------------------- /test/example-text/brackets-comments2.txt: -------------------------------------------------------------------------------- 1 | {} 2 | /*{{{*/ 3 | // {}{}{{([])}} -------------------------------------------------------------------------------- /test/example-text/brackets-unbalanced3.txt: -------------------------------------------------------------------------------- 1 | { 2 | { 3 | { 4 | TEXT[ 5 | } 6 | { 7 | TEXT] 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /test/example-text/brackets-mixed.txt: -------------------------------------------------------------------------------- 1 | { 2 | { 3 | { 4 | TEXT 5 | } 6 | { 7 | TEXT 8 | } 9 | } 10 | 11 | a { 12 | 13 | } 14 | 15 | a [ 16 | 17 | ] 18 | 19 | a ( 20 | 21 | ) 22 | } -------------------------------------------------------------------------------- /test/example-text/brackets-unbalanced.txt: -------------------------------------------------------------------------------- 1 | }GARBAGE{TEXT}GARBAGE 2 | GARBAGE 3 | GARBAGE{ 4 | TEXT 5 | }GARBAGE 6 | GARBAGE 7 | GARBAGE{{{TEXT}}}GARBAGE 8 | GARBAGE{ 9 | { 10 | { 11 | TEXT 12 | } 13 | } 14 | }GARBAGE 15 | GARBAGE -------------------------------------------------------------------------------- /test/example-text/brackets-unbalanced2.txt: -------------------------------------------------------------------------------- 1 | }{GARBAGE{TEXT}GARBAGE 2 | GARBAGE 3 | GARBAGE{ 4 | TEXT 5 | }GARBAGE 6 | GARBAGE 7 | GARBAGE{{{TEXT}}}GARBAGE 8 | GARBAGE{ 9 | { 10 | { 11 | TEXT 12 | } 13 | } 14 | }GARBAGE 15 | GARBAGE -------------------------------------------------------------------------------- /test/example-text/brackets-unbalanced4.txt: -------------------------------------------------------------------------------- 1 | { 2 | { 3 | { 4 | TEXT[ 5 | } 6 | { 7 | TEXT] 8 | } 9 | } 10 | 11 | a { 12 | 13 | } 14 | 15 | a [ 16 | 17 | ] 18 | 19 | a ( 20 | 21 | ) 22 | } 23 | ( 24 | ( 25 | ( 26 | TEXT[ 27 | ) 28 | ( 29 | TEXT] 30 | ) 31 | ) 32 | 33 | a { 34 | 35 | } 36 | 37 | a [ 38 | 39 | ] 40 | 41 | a ( 42 | 43 | ) 44 | ) -------------------------------------------------------------------------------- /test/example-text/brackets-head.txt: -------------------------------------------------------------------------------- 1 | GARBAGE head ( 2 | ( 3 | ( 4 | TEXT 5 | ) 6 | ) 7 | head () 8 | )GARBAGE 9 | GARBAGE head2 ( 10 | ( 11 | ( 12 | TEXT 13 | ) 14 | ) 15 | head2 () 16 | )GARBAGE 17 | GARBAGE head ( 18 | ( 19 | ( 20 | TEXT 21 | ) 22 | ) 23 | head () 24 | )GARBAGE 25 | GARBAGE head2 ( 26 | ( 27 | ( 28 | TEXT 29 | ) 30 | ) 31 | head2 () 32 | )GARBAGE -------------------------------------------------------------------------------- /test/example-text/brackets-comments.txt: -------------------------------------------------------------------------------- 1 | { 2 | { 3 | { 4 | TEXT 5 | } 6 | { 7 | TEXT 8 | } 9 | } 10 | 11 | a { 12 | 13 | } 14 | 15 | a [ 16 | 17 | ] 18 | 19 | a ( 20 | 21 | ) 22 | } 23 | // {{{TEXT}{TEXT}}a{}a[]a()} 24 | /* 25 | { 26 | { 27 | { 28 | TEXT 29 | 30 | { 31 | TEXT 32 | } 33 | } 34 | 35 | a { 36 | 37 | } 38 | 39 | a [ 40 | 41 | 42 | 43 | a ( 44 | 45 | ) 46 | } 47 | */ -------------------------------------------------------------------------------- /test/example-text/brackets-unbalanced5.txt: -------------------------------------------------------------------------------- 1 | { 2 | 3 | { 4 | { 5 | TEXT[ 6 | } 7 | { 8 | TEXT] 9 | } 10 | } 11 | 12 | a { 13 | 14 | } 15 | 16 | 17 | a [ 18 | 19 | ] 20 | 21 | a ( 22 | 23 | 24 | ) 25 | } 26 | 27 | 28 | 29 | ( 30 | ( 31 | ( 32 | TEXT[ 33 | ) 34 | ( 35 | TEXT] 36 | ) 37 | ) 38 | 39 | a { 40 | 41 | } 42 | 43 | a [ 44 | 45 | ] 46 | 47 | a ( 48 | 49 | ) 50 | ) 51 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-balanced", 3 | "version": "0.0.16", 4 | "description": "balanced string matching, and replacing.", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "grunt test" 8 | }, 9 | "author": { 10 | "name": "Chad Scira", 11 | "email": "chadvscira@gmail.com" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git://github.com/icodeforlove/node-balanced.git" 16 | }, 17 | "license": "MIT", 18 | "devDependencies": { 19 | "vows": "~0.7.0", 20 | "grunt": "~0.4.5", 21 | "webpack": "~1.3.2-beta9", 22 | "webpack-dev-server": "~1.4.7", 23 | "grunt-webpack": "~1.0.7", 24 | "grunt-contrib-uglify": "~0.5.1", 25 | "grunt-banner": "~0.2.3", 26 | "grunt-contrib-jshint": "~0.10.0", 27 | "grunt-contrib-watch": "~0.6.1", 28 | "grunt-jasmine-node": "~0.2.1", 29 | "grunt-cli": "~0.1.13" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /test/example-text/brackets-basic.txt: -------------------------------------------------------------------------------- 1 | GARBAGE{TEXT}GARBAGE 2 | GARBAGE 3 | GARBAGE{ 4 | TEXT 5 | }GARBAGE 6 | GARBAGE 7 | GARBAGE{{{TEXT}}}GARBAGE 8 | GARBAGE{ 9 | { 10 | { 11 | TEXT 12 | } 13 | } 14 | }GARBAGE 15 | GARBAGE 16 | GARBAGE{ 17 | {TEXT} 18 | {TEXT} 19 | }GARBAGE 20 | GARBAGE 21 | GARBAGE(TEXT)GARBAGE 22 | GARBAGE 23 | GARBAGE( 24 | TEXT 25 | )GARBAGE 26 | GARBAGE 27 | GARBAGE(((TEXT)))GARBAGE 28 | GARBAGE( 29 | ( 30 | ( 31 | TEXT 32 | ) 33 | ) 34 | )GARBAGE 35 | GARBAGE 36 | GARBAGE( 37 | (TEXT) 38 | (TEXT) 39 | )GARBAGE 40 | GARBAGE 41 | GARBAGE[TEXT]GARBAGE 42 | GARBAGE 43 | GARBAGE[ 44 | TEXT 45 | ]GARBAGE 46 | GARBAGE 47 | GARBAGE[[[TEXT]]]GARBAGE 48 | GARBAGE[ 49 | [ 50 | [ 51 | TEXT 52 | ] 53 | ] 54 | ]GARBAGE 55 | GARBAGE 56 | GARBAGE[ 57 | [TEXT] 58 | [TEXT] 59 | ]GARBAGE 60 | GARBAGE 61 | GARBAGETEXTGARBAGE 62 | GARBAGE 63 | GARBAGE 64 | TEXT 65 | GARBAGE 66 | GARBAGE 67 | GARBAGETEXTGARBAGE 68 | GARBAGE 69 | 70 | 71 | TEXT 72 | 73 | 74 | GARBAGE 75 | GARBAGE 76 | GARBAGE 77 | TEXT 78 | TEXT 79 | GARBAGE 80 | GARBAGE -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | grunt.initConfig({ 3 | pkg: grunt.file.readJSON('package.json'), 4 | 5 | webpack: { 6 | build: { 7 | devtool: 'source-map', 8 | entry: './index.js', 9 | output: { 10 | library: 'balanced', 11 | path: 'dist/', 12 | filename: 'balanced.js' 13 | } 14 | } 15 | }, 16 | 17 | uglify: { 18 | build: { 19 | files: { 20 | 'dist/balanced-min.js': ['dist/balanced.js'] 21 | } 22 | } 23 | }, 24 | 25 | banner: '/**\n * balanced.js v<%= pkg.version %>\n */', 26 | usebanner: { 27 | dist: { 28 | options: { 29 | position: 'top', 30 | banner: '<%= banner %>' 31 | }, 32 | files: { 33 | 'dist/balanced.js': ['dist/balanced.js'], 34 | 'dist/balanced-min.js': ['dist/balanced-min.js'] 35 | } 36 | } 37 | }, 38 | 39 | jshint: { 40 | options: { 41 | jshintrc: true 42 | }, 43 | all: ['./*.js', './test/*.js'] 44 | }, 45 | 46 | jasmine_node: { 47 | options: { 48 | forceExit: true, 49 | match: '.', 50 | matchall: false, 51 | extensions: 'js', 52 | specNameMatcher: 'test' 53 | }, 54 | all: ['test/'] 55 | }, 56 | watch: { 57 | jasmine: { 58 | files: ['test/*.js'], 59 | tasks: ['test'] 60 | } 61 | } 62 | }); 63 | 64 | grunt.loadNpmTasks('grunt-webpack'); 65 | grunt.loadNpmTasks('grunt-contrib-uglify'); 66 | grunt.loadNpmTasks('grunt-banner'); 67 | grunt.loadNpmTasks('grunt-contrib-jshint'); 68 | grunt.loadNpmTasks('grunt-jasmine-node'); 69 | grunt.loadNpmTasks('grunt-contrib-watch'); 70 | 71 | grunt.registerTask('build', ['jshint', 'webpack', 'uglify', 'usebanner']); 72 | grunt.registerTask('test', ['jshint', 'jasmine_node']); 73 | }; -------------------------------------------------------------------------------- /dist/balanced-min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * balanced.js v0.0.16 3 | */ 4 | var balanced=function(a){function b(d){if(c[d])return c[d].exports;var e=c[d]={exports:{},id:d,loaded:!1};return a[d].call(e.exports,e,e.exports,b),e.loaded=!0,e.exports}var c={};return b.m=a,b.c=c,b.p="",b(0)}([function(a,b,c){function d(a){if(a=a||{},!a.open)throw new Error('Balanced: please provide a "open" property');if(!a.close)throw new Error('Balanced: please provide a "close" property');if(this.balance=a.balance||!1,this.exceptions=a.exceptions||!1,this.caseInsensitive=a.caseInsensitive,this.head=a.head||a.open,this.head=Array.isArray(this.head)?this.head:[this.head],this.open=Array.isArray(a.open)?a.open:[a.open],this.close=Array.isArray(a.close)?a.close:[a.close],!Array.isArray(this.head)||!Array.isArray(this.open)||!Array.isArray(this.close)||this.head.length!==this.open.length||this.open.length!==this.close.length)throw new Error('Balanced: if you use arrays for a "head,open,close" you must use matching arrays for all options');var b=k(this.head.map(this.regExpFromArrayGroupedMap,this)),c=k(this.open.map(this.regExpFromArrayGroupedMap,this)),d=k(this.close.map(this.regExpFromArrayGroupedMap,this));this.regExp=k([b,c,d],"g"+(this.caseInsensitive?"i":"")),this.regExpGroupLength=this.head.length}function e(a,b,c){return a.toString().length=0;n--)g-n>=0&&f[g-n]&&(k+=e(g-n+1,m," ")+": "+b.substr(f[g-n].index,f[g-n].length).replace(/\n/g,"")+"\n");for(n=0;j-1+(m+2)>n;n++)k+="-";for(k+="^\n",n=1;l>=n;n++)g+n>=0&&f[g+n]&&(k+=e(g+n+1,m," ")+": "+b.substr(f[g+n].index,f[g+n].length).replace(/\n/g,"")+"\n");k=k.replace(/\t/g," ").replace(/\n$/,"");var o=new Error(a+" at "+(g+1)+":"+j+"\n\n"+k);return o.line=g+1,o.column=j,o.index=c,o}function g(a,b){return a>=b.index&&a<=b.index+b.length-1}function h(a,b){var c,d=new RegExp(b),e=[];if(a)for(;c=d.exec(a);)e.push({index:c.index,length:c[0].length,match:c[0]}),c[0].length||d.lastIndex++;return e}function i(a,b,c){var d=0;if(!a)return b;for(var e=0;e]*class="[^"]*myclassname[^"]*"[^>]*>/, 62 | open: /]*>/, 63 | close: '' 64 | }); 65 | ``` 66 | ## matching 67 | 68 | you can get balanced matches by doing the following 69 | 70 | ```javascript 71 | var balanced = require('node-balanced'); 72 | 73 | balanced.matches({ 74 | source: source, 75 | head: /@hello \d \{/, // optional (defalut: open) 76 | open: '{', 77 | close: '}', 78 | balance: false, // optional (default: false) when set to true it will return `null` when there is an error 79 | exceptions: false // optional (default: false), 80 | ignore: [] // array of ignore ranges/matches 81 | }); 82 | ``` 83 | 84 | ## multiple head/open/close 85 | 86 | you can match multiple head/open/close efficiently by doing this 87 | 88 | ```javascript 89 | var isBalanced = balanced.matches({ 90 | source: '{[({)]}}', 91 | open: ['{', '[', '('], 92 | close: ['}', ']', ')'], 93 | balance: true 94 | }); 95 | ``` 96 | ## ignore 97 | ignore is supported by the `matches` and `replacements` methods, this is very useful for something like not matching inside of comments 98 | 99 | ```javascript 100 | var blockComments = balanced.matches({source: source, open: '/*', close: '*/'}), 101 | singleLineComments = balanced.getRangesForMatch(source, /^\s*\/\/.+$/gim); 102 | 103 | balanced.matches({ 104 | source: source, 105 | head: /@hello \d \{/, 106 | open: '{', 107 | close: '}', 108 | ignore: Array.prototype.concat.call([], blockComments, singleLineComments), 109 | replace: function (source, head, tail) { 110 | return head + source + tail; 111 | } 112 | }); 113 | ``` 114 | 115 | ## advanced 116 | 117 | in this example we have code and we want to avoid replacing text thats inside of the multiline/singleline comments, and quotes 118 | 119 | ```css 120 | { 121 | @hello 1 { 122 | a { 123 | } 124 | } 125 | /* 126 | @hello 2 { 127 | a { 128 | } 129 | } 130 | */ 131 | @hello 3 { 132 | a { 133 | } 134 | } 135 | // @hello 4 {} 136 | } 137 | 138 | var hello = "@hello 5 {}"; 139 | ``` 140 | 141 | with balanced you can do this 142 | 143 | ```javascript 144 | // returns quote ranges with option ignore filter 145 | function getQuoteRanges (string, ignore) { 146 | var quotes = balanced.getRangesForMatch(string, new RegExp('\'|"', 'g')); 147 | 148 | // filter out ingored ranges 149 | if (ignore) { 150 | quotes = balanced.rangesWithout(quotes, ignore); 151 | } 152 | 153 | var currect = null, 154 | ranges = []; 155 | 156 | quotes.forEach(function (quote) { 157 | if (currect && currect.match === quote.match) { 158 | ranges.push({ 159 | index: currect.index, 160 | length: quote.index - currect.index + 1 161 | }); 162 | currect = null; 163 | } else if (!currect) { 164 | currect = quote; 165 | } 166 | }); 167 | 168 | return ranges; 169 | } 170 | 171 | var blockComments = balanced.matches({source: string, open: '/*', close: '*/'}), 172 | singleLineComments = balanced.getRangesForMatch(string, /^\s*\/\/.+$/gim), 173 | ignores = Array.prototype.concat.call([], blockComments, singleLineComments), 174 | quotes = getQuoteRanges(string, ignores); 175 | 176 | // remove ignores inside of quotes 177 | ignores = balanced.rangesWithout(ignores, quotes); 178 | 179 | // optional ignore code inside of quotes 180 | ignores = ignores.concat(quotes); 181 | 182 | // run your matches or replacements method 183 | balanced.matches({ 184 | source: string, 185 | head: /@hello \d \{/, 186 | open: '{', 187 | close: '}', 188 | ignore: ignores 189 | }); 190 | ``` 191 | 192 | as you can see by using these principles you can accomplish this kind of stuff easily 193 | -------------------------------------------------------------------------------- /test/balance.test.js: -------------------------------------------------------------------------------- 1 | var balanced = require('../index'), 2 | fs = require('fs'); 3 | 4 | var examples = { 5 | bracketsUnbalanced: fs.readFileSync(__dirname + '/example-text/brackets-unbalanced.txt', 'utf8'), 6 | bracketsUnbalanced2: fs.readFileSync(__dirname + '/example-text/brackets-unbalanced2.txt', 'utf8'), 7 | bracketsUnbalanced3: fs.readFileSync(__dirname + '/example-text/brackets-unbalanced3.txt', 'utf8'), 8 | bracketsUnbalanced4: fs.readFileSync(__dirname + '/example-text/brackets-unbalanced4.txt', 'utf8'), 9 | bracketsUnbalanced5: fs.readFileSync(__dirname + '/example-text/brackets-unbalanced5.txt', 'utf8') 10 | }; 11 | 12 | describe('Balancing', function() { 13 | it('can perform a simple balance check', function() { 14 | var matches = balanced.matches({source: examples.bracketsUnbalanced, open: '{', close: '}', balance: true}); 15 | expect(matches).toEqual(null); 16 | }); 17 | 18 | it('can perform a more exact balance check', function() { 19 | var matches = balanced.matches({source: examples.bracketsUnbalanced2, open: '{', close: '}', balance: true}); 20 | expect(matches).toEqual(null); 21 | }); 22 | 23 | it('can match unbalanced source', function() { 24 | var matches = balanced.matches({source: examples.bracketsUnbalanced, open: '{', close: '}', balance: false}); 25 | 26 | expect(matches).toEqual([ 27 | { index: 8, length: 6, head: '{', tail: '}' }, 28 | { index: 37, length: 9, head: '{', tail: '}' }, 29 | { index: 69, length: 10, head: '{', tail: '}' }, 30 | { index: 94, length: 25, head: '{', tail: '}' } 31 | ]); 32 | }); 33 | 34 | it('can replace while unbalanced', function () { 35 | expect(balanced.replacements({ 36 | source: 'unbalanced{source', 37 | open: ['{'], 38 | close: ['}'], 39 | balance: true, 40 | exceptions: false, 41 | replace: function () { 42 | return ''; 43 | } 44 | })).toEqual('unbalanced{source'); 45 | }); 46 | 47 | it('can replace with numbers', function () { 48 | expect(balanced.replacements({ 49 | source: "{?}{?}", 50 | open: "{", 51 | close: "}", 52 | replace: function(match, head, tail) { 53 | return 1; 54 | } 55 | })).toEqual('11'); 56 | }); 57 | 58 | 59 | it('can match bad unbalanced source', function() { 60 | var matches = balanced.matches({source: examples.bracketsUnbalanced2, open: '{', close: '}', balance: false}); 61 | expect(matches).toEqual([]); 62 | }); 63 | 64 | it('can match throw error for unbalanced source', function() { 65 | var errorMessage; 66 | try { 67 | balanced.matches({source: examples.bracketsUnbalanced, open: '{', close: '}', balance: true, exceptions: true}); 68 | } catch (error) { 69 | errorMessage = error.message; 70 | } 71 | 72 | expect(errorMessage).toEqual( 73 | 'Balanced: unexpected close bracket at 1:1\n\n1: }GARBAGE{TEXT}GARBAGE\n---^\n2: GARBAGE\n3: GARBAGE{' 74 | ); 75 | }); 76 | 77 | it('can match throw error for bad unbalanced source', function() { 78 | var errorMessage; 79 | try { 80 | balanced.matches({source: examples.bracketsUnbalanced2, open: '{', close: '}', balance: true, exceptions: true}); 81 | } catch (error) { 82 | errorMessage = error.message; 83 | } 84 | 85 | expect(errorMessage).toEqual( 86 | 'Balanced: unexpected close bracket at 1:1\n\n1: }{GARBAGE{TEXT}GARBAGE\n---^\n2: GARBAGE\n3: GARBAGE{' 87 | ); 88 | }); 89 | 90 | it('can perform a balance check with multiple open/close', function () { 91 | var errorMessage; 92 | try { 93 | balanced.matches({ 94 | source: examples.bracketsUnbalanced3, 95 | head: ['{', '[', '('], 96 | open: ['{', '[', '('], 97 | close: ['}', ']', ')'], 98 | balance: true, 99 | exceptions: true 100 | }); 101 | } catch (error) { 102 | errorMessage = error.message; 103 | } 104 | 105 | expect(errorMessage).toEqual( 106 | 'Balanced: mismatching close bracket, expected \"]\" but found \"}\" at 5:3\n\n3: {\n4: TEXT[\n5: }\n-----^\n6: {\n7: TEXT]' 107 | ); 108 | }); 109 | 110 | it('can perform an unbalanced match with multiple open/close', function () { 111 | expect(balanced.matches({ 112 | source: examples.bracketsUnbalanced4, 113 | open: ['{', '[', '('], 114 | close: ['}', ']', ')'] 115 | })).toEqual([ 116 | { index: 0, length: 73, head: '{', tail: '}' }, 117 | { index: 74, length: 73, head: '(', tail: ')' } 118 | ]); 119 | }); 120 | 121 | it('can perform an unbalanced match with multiple head/open/close', function () { 122 | expect(balanced.matches({ 123 | source: examples.bracketsUnbalanced4, 124 | head: ['a {', 'a [', 'a ('], 125 | open: ['{', '[', '('], 126 | close: ['}', ']', ')'], 127 | })).toEqual([ 128 | { index: 44, length: 7, head: 'a {', tail: '}' }, 129 | { index: 54, length: 7, head: 'a [', tail: ']' }, 130 | { index: 64, length: 7, head: 'a (', tail: ')' }, 131 | { index: 118, length: 7, head: 'a {', tail: '}' }, 132 | { index: 128, length: 7, head: 'a [', tail: ']' }, 133 | { index: 138, length: 7, head: 'a (', tail: ')' } 134 | ]); 135 | }); 136 | 137 | it('can perform a balance check with multiple open/close, and multiple line returns', function () { 138 | var errorMessage; 139 | try { 140 | balanced.matches({ 141 | source: examples.bracketsUnbalanced5, 142 | head: ['{', '[', '('], 143 | open: ['{', '[', '('], 144 | close: ['}', ']', ')'], 145 | balance: true, 146 | exceptions: true 147 | }); 148 | } catch (error) { 149 | errorMessage = error.message; 150 | } 151 | 152 | expect(errorMessage).toEqual( 153 | 'Balanced: mismatching close bracket, expected \"]\" but found \"}\" at 6:3\n\n4: {\n5: TEXT[\n6: }\n-----^\n7: {\n8: TEXT]' 154 | ); 155 | }); 156 | }); -------------------------------------------------------------------------------- /test/matches.test.js: -------------------------------------------------------------------------------- 1 | var balanced = require('../index'), 2 | fs = require('fs'); 3 | 4 | var examples = { 5 | bracketsBasic: fs.readFileSync(__dirname + '/example-text/brackets-basic.txt', 'utf8'), 6 | bracketsHead: fs.readFileSync(__dirname + '/example-text/brackets-head.txt', 'utf8'), 7 | comments: fs.readFileSync(__dirname + '/example-text/brackets-comments.txt', 'utf8'), 8 | comments2: fs.readFileSync(__dirname + '/example-text/brackets-comments2.txt', 'utf8') 9 | }; 10 | 11 | describe('Matches', function() { 12 | it('can perform simple string matches', function() { 13 | expect(balanced.matches({source: examples.bracketsBasic, open: '{', close: '}'})).toEqual([ 14 | { index: 7, length: 6, head: '{', tail: '}' }, 15 | { index: 36, length: 9, head: '{', tail: '}' }, 16 | { index: 68, length: 10, head: '{', tail: '}' }, 17 | { index: 93, length: 25, head: '{', tail: '}' }, 18 | { index: 141, length: 19, head: '{', tail: '}' } 19 | ]); 20 | 21 | expect(balanced.matches({source: examples.bracketsBasic, open: '(', close: ')'})).toEqual([ 22 | { index: 183, length: 6, head: '(', tail: ')' }, 23 | { index: 212, length: 9, head: '(', tail: ')' }, 24 | { index: 244, length: 10, head: '(', tail: ')' }, 25 | { index: 269, length: 25, head: '(', tail: ')' }, 26 | { index: 317, length: 19, head: '(', tail: ')' } 27 | ]); 28 | 29 | expect(balanced.matches({source: examples.bracketsBasic, open: '[', close: ']'})).toEqual([ 30 | { index: 359, length: 6, head: '[', tail: ']' }, 31 | { index: 388, length: 9, head: '[', tail: ']' }, 32 | { index: 420, length: 10, head: '[', tail: ']' }, 33 | { index: 445, length: 25, head: '[', tail: ']' }, 34 | { index: 493, length: 19, head: '[', tail: ']' } 35 | ]); 36 | 37 | expect(balanced.matches({source: examples.bracketsBasic, open: '', close: ''})).toEqual([ 38 | { index: 535, length: 15, head: '', tail: '' }, 39 | { index: 573, length: 18, head: '', tail: '' }, 40 | { index: 614, length: 37, head: '', tail: '' }, 41 | { index: 666, length: 52, head: '', tail: '' }, 42 | { index: 741, length: 46, head: '', tail: '' } 43 | ]); 44 | }); 45 | 46 | it('can perform simple regexp matches', function() { 47 | expect(balanced.matches({source: examples.bracketsBasic, open: /\[|\{|\(|/, close: /\]|\}|\)|<\/tag>/})).toEqual([ 48 | { index: 7, length: 6, head: '{', tail: '}' }, 49 | { index: 36, length: 9, head: '{', tail: '}' }, 50 | { index: 68, length: 10, head: '{', tail: '}' }, 51 | { index: 93, length: 25, head: '{', tail: '}' }, 52 | { index: 141, length: 19, head: '{', tail: '}' }, 53 | { index: 183, length: 6, head: '(', tail: ')' }, 54 | { index: 212, length: 9, head: '(', tail: ')' }, 55 | { index: 244, length: 10, head: '(', tail: ')' }, 56 | { index: 269, length: 25, head: '(', tail: ')' }, 57 | { index: 317, length: 19, head: '(', tail: ')' }, 58 | { index: 359, length: 6, head: '[', tail: ']' }, 59 | { index: 388, length: 9, head: '[', tail: ']' }, 60 | { index: 420, length: 10, head: '[', tail: ']' }, 61 | { index: 445, length: 25, head: '[', tail: ']' }, 62 | { index: 493, length: 19, head: '[', tail: ']' }, 63 | { index: 535, length: 15, head: '', tail: '' }, 64 | { index: 573, length: 18, head: '', tail: '' }, 65 | { index: 614, length: 37, head: '', tail: '' }, 66 | { index: 666, length: 52, head: '', tail: '' }, 67 | { index: 741, length: 46, head: '', tail: '' } 68 | ]); 69 | }); 70 | 71 | it('can perform head matches', function () { 72 | expect(balanced.matches({source: examples.bracketsHead, head: 'head (', open: '(', close: ')'})).toEqual([ 73 | { index: 8, length: 39, head: 'head (', tail: ')' }, 74 | { index: 120, length: 39, head: 'head (', tail: ')' } 75 | ]); 76 | }); 77 | 78 | it('can perform regexp head matches', function () { 79 | expect(balanced.matches({source: examples.bracketsHead, head: /head\d? \(/, open: '(', close: ')'})).toEqual([ 80 | { index: 8, length: 39, head: 'head (', tail: ')' }, 81 | { index: 63, length: 41, head: 'head2 (', tail: ')' }, 82 | { index: 120, length: 39, head: 'head (', tail: ')' }, 83 | { index: 175, length: 41, head: 'head2 (', tail: ')' } 84 | ]); 85 | }); 86 | 87 | it('can ignore matches', function () { 88 | var blockComments = balanced.matches({source: examples.comments, open: '/*', close: '*/'}), 89 | singleLineComments = balanced.getRangesForMatch(examples.comments, /^\s*\/\/.+$/gim); 90 | 91 | expect(balanced.matches({ 92 | source: examples.comments, 93 | open: ['{', '[', '('], 94 | close: ['}', ']', ')'], 95 | ignore: Array.prototype.concat.call([], blockComments, singleLineComments) 96 | })).toEqual([ 97 | { index: 0, length: 71, head: '{', tail: '}' } 98 | ]); 99 | 100 | expect(balanced.matches({ 101 | source: examples.comments, 102 | open: ['{', '[', '('], 103 | close: ['}', ']', ')'], 104 | ignore: blockComments 105 | })).toEqual([ 106 | { index: 0, length: 71, head: '{', tail: '}' }, 107 | { index: 75, length: 25, head: '{', tail: '}' } 108 | ]); 109 | }); 110 | 111 | it('can ignore matches 2', function () { 112 | var blockComments = balanced.matches({source: examples.comments2, open: '/*', close: '*/'}), 113 | singleLineComments = balanced.getRangesForMatch(examples.comments2, /^\s*\/\/.+$/gim); 114 | 115 | expect(balanced.matches({ 116 | source: examples.comments2, 117 | open: ['{', '[', '('], 118 | close: ['}', ']', ')'], 119 | ignore: blockComments 120 | })).toEqual([ 121 | { index: 0, length: 2, head: '{', tail: '}' }, 122 | { index: 14, length: 2, head: '{', tail: '}' }, 123 | { index: 16, length: 2, head: '{', tail: '}' }, 124 | { index: 18, length: 8, head: '{', tail: '}' } 125 | ]); 126 | 127 | expect(balanced.matches({ 128 | source: examples.comments2, 129 | open: ['{', '[', '('], 130 | close: ['}', ']', ')'], 131 | ignore: Array.prototype.concat.call([], blockComments, singleLineComments) 132 | })).toEqual([ 133 | { index: 0, length: 2, head: '{', tail: '}' } 134 | ]); 135 | }); 136 | 137 | it('can match with complex custom ignore ', function () { 138 | function getQuoteRanges (string, ignore) { 139 | var quotes = balanced.getRangesForMatch(string, new RegExp('\'|"', 'g')); 140 | 141 | // filter out ingored ranges 142 | if (ignore) { 143 | quotes = balanced.rangesWithout(quotes, ignore); 144 | } 145 | 146 | var currect = null, 147 | ranges = []; 148 | 149 | quotes.forEach(function (quote) { 150 | if (currect && currect.match === quote.match) { 151 | ranges.push({ 152 | index: currect.index, 153 | length: quote.index - currect.index + 1 154 | }); 155 | currect = null; 156 | } else if (!currect) { 157 | currect = quote; 158 | } 159 | }); 160 | 161 | return ranges; 162 | } 163 | 164 | var string = 165 | '/* {}" */\n' + 166 | '/* {}\' */\n' + 167 | '// {}"\n' + 168 | '// {}\'\n' + 169 | '{}\n' + 170 | '" /*{}*/ "\n' + 171 | '\' /*{}*/ \'\n' + 172 | '/* """ */\n' + 173 | '/* \'\'\' */\n' + 174 | '" {} "\n' + 175 | '\' {} \'\n' + 176 | '/* """ */\n' + 177 | '/* \'\'\' */\n'; 178 | 179 | var blockComments = balanced.matches({source: string, open: '/*', close: '*/'}), 180 | singleLineComments = balanced.getRangesForMatch(string, /^\s*\/\/.+$/gim), 181 | ignores = Array.prototype.concat.call([], blockComments, singleLineComments), 182 | quotes = getQuoteRanges(string, ignores); 183 | 184 | // remove ignores inside of quotes 185 | ignores = balanced.rangesWithout(ignores, quotes); 186 | 187 | // option ignore code inside of quotes 188 | ignores = ignores.concat(quotes); 189 | 190 | expect(balanced.matches({ 191 | source: string, 192 | open: ['{', '[', '('], 193 | close: ['}', ']', ')'], 194 | ignore: ignores 195 | })).toEqual([ 196 | { index : 34, length : 2, head : '{', tail : '}' } 197 | ]); 198 | }); 199 | }); -------------------------------------------------------------------------------- /test/replacements.test.js: -------------------------------------------------------------------------------- 1 | var balanced = require('../index'), 2 | fs = require('fs'); 3 | 4 | var examples = { 5 | bracketsBasic: fs.readFileSync(__dirname + '/example-text/brackets-basic.txt', 'utf8'), 6 | bracketsHead: fs.readFileSync(__dirname + '/example-text/brackets-head.txt', 'utf8'), 7 | comments: fs.readFileSync(__dirname + '/example-text/brackets-comments.txt', 'utf8') 8 | }; 9 | 10 | describe('Replacements', function() { 11 | it('can perform simple string replacements', function() { 12 | expect(balanced.replacements({source: examples.bracketsBasic, open: '{', close: '}', replace: function (source, head, tail) { 13 | return '' + source + ''; 14 | }})).toEqual( 15 | 'GARBAGETEXTGARBAGE\nGARBAGE\nGARBAGE\n\tTEXT\nGARBAGE\nGARBAGE\nGARBAGE{{TEXT}}GARBAGE\nGARBAGE\n\t{\n\t\t{\n\t\t\tTEXT\n\t\t}\n\t}\nGARBAGE\nGARBAGE\nGARBAGE\n\t{TEXT}\n\t{TEXT}\nGARBAGE\nGARBAGE\nGARBAGE(TEXT)GARBAGE\nGARBAGE\nGARBAGE(\n\tTEXT\n)GARBAGE\nGARBAGE\nGARBAGE(((TEXT)))GARBAGE\nGARBAGE(\n\t(\n\t\t(\n\t\t\tTEXT\n\t\t)\n\t)\n)GARBAGE\nGARBAGE\nGARBAGE(\n\t(TEXT)\n\t(TEXT)\n)GARBAGE\nGARBAGE\nGARBAGE[TEXT]GARBAGE\nGARBAGE\nGARBAGE[\n\tTEXT\n]GARBAGE\nGARBAGE\nGARBAGE[[[TEXT]]]GARBAGE\nGARBAGE[\n\t[\n\t\t[\n\t\t\tTEXT\n\t\t]\n\t]\n]GARBAGE\nGARBAGE\nGARBAGE[\n\t[TEXT]\n\t[TEXT]\n]GARBAGE\nGARBAGE\nGARBAGETEXTGARBAGE\nGARBAGE\nGARBAGE\n\tTEXT\nGARBAGE\nGARBAGE\nGARBAGETEXTGARBAGE\nGARBAGE\n\t\n\t\t\n\t\t\tTEXT\n\t\t\n\t\nGARBAGE\nGARBAGE\nGARBAGE\n\tTEXT\n\tTEXT\nGARBAGE\nGARBAGE' 16 | ); 17 | 18 | expect(balanced.replacements({source: examples.bracketsBasic, open: '(', close: ')', replace: function (source, head, tail) { 19 | return '' + source + ''; 20 | }})).toEqual( 21 | 'GARBAGE{TEXT}GARBAGE\nGARBAGE\nGARBAGE{\n\tTEXT\n}GARBAGE\nGARBAGE\nGARBAGE{{{TEXT}}}GARBAGE\nGARBAGE{\n\t{\n\t\t{\n\t\t\tTEXT\n\t\t}\n\t}\n}GARBAGE\nGARBAGE\nGARBAGE{\n\t{TEXT}\n\t{TEXT}\n}GARBAGE\nGARBAGE\nGARBAGETEXTGARBAGE\nGARBAGE\nGARBAGE\n\tTEXT\nGARBAGE\nGARBAGE\nGARBAGE((TEXT))GARBAGE\nGARBAGE\n\t(\n\t\t(\n\t\t\tTEXT\n\t\t)\n\t)\nGARBAGE\nGARBAGE\nGARBAGE\n\t(TEXT)\n\t(TEXT)\nGARBAGE\nGARBAGE\nGARBAGE[TEXT]GARBAGE\nGARBAGE\nGARBAGE[\n\tTEXT\n]GARBAGE\nGARBAGE\nGARBAGE[[[TEXT]]]GARBAGE\nGARBAGE[\n\t[\n\t\t[\n\t\t\tTEXT\n\t\t]\n\t]\n]GARBAGE\nGARBAGE\nGARBAGE[\n\t[TEXT]\n\t[TEXT]\n]GARBAGE\nGARBAGE\nGARBAGETEXTGARBAGE\nGARBAGE\nGARBAGE\n\tTEXT\nGARBAGE\nGARBAGE\nGARBAGETEXTGARBAGE\nGARBAGE\n\t\n\t\t\n\t\t\tTEXT\n\t\t\n\t\nGARBAGE\nGARBAGE\nGARBAGE\n\tTEXT\n\tTEXT\nGARBAGE\nGARBAGE' 22 | ); 23 | 24 | expect(balanced.replacements({source: examples.bracketsBasic, open: '[', close: ']', replace: function (source, head, tail) { 25 | return '' + source + ''; 26 | }})).toEqual( 27 | 'GARBAGE{TEXT}GARBAGE\nGARBAGE\nGARBAGE{\n\tTEXT\n}GARBAGE\nGARBAGE\nGARBAGE{{{TEXT}}}GARBAGE\nGARBAGE{\n\t{\n\t\t{\n\t\t\tTEXT\n\t\t}\n\t}\n}GARBAGE\nGARBAGE\nGARBAGE{\n\t{TEXT}\n\t{TEXT}\n}GARBAGE\nGARBAGE\nGARBAGE(TEXT)GARBAGE\nGARBAGE\nGARBAGE(\n\tTEXT\n)GARBAGE\nGARBAGE\nGARBAGE(((TEXT)))GARBAGE\nGARBAGE(\n\t(\n\t\t(\n\t\t\tTEXT\n\t\t)\n\t)\n)GARBAGE\nGARBAGE\nGARBAGE(\n\t(TEXT)\n\t(TEXT)\n)GARBAGE\nGARBAGE\nGARBAGETEXTGARBAGE\nGARBAGE\nGARBAGE\n\tTEXT\nGARBAGE\nGARBAGE\nGARBAGE[[TEXT]]GARBAGE\nGARBAGE\n\t[\n\t\t[\n\t\t\tTEXT\n\t\t]\n\t]\nGARBAGE\nGARBAGE\nGARBAGE\n\t[TEXT]\n\t[TEXT]\nGARBAGE\nGARBAGE\nGARBAGETEXTGARBAGE\nGARBAGE\nGARBAGE\n\tTEXT\nGARBAGE\nGARBAGE\nGARBAGETEXTGARBAGE\nGARBAGE\n\t\n\t\t\n\t\t\tTEXT\n\t\t\n\t\nGARBAGE\nGARBAGE\nGARBAGE\n\tTEXT\n\tTEXT\nGARBAGE\nGARBAGE' 28 | ); 29 | 30 | expect(balanced.replacements({source: examples.bracketsBasic, open: '', close: '', replace: function (source, head, tail) { 31 | return '' + source + ''; 32 | }})).toEqual( 33 | 'GARBAGE{TEXT}GARBAGE\nGARBAGE\nGARBAGE{\n\tTEXT\n}GARBAGE\nGARBAGE\nGARBAGE{{{TEXT}}}GARBAGE\nGARBAGE{\n\t{\n\t\t{\n\t\t\tTEXT\n\t\t}\n\t}\n}GARBAGE\nGARBAGE\nGARBAGE{\n\t{TEXT}\n\t{TEXT}\n}GARBAGE\nGARBAGE\nGARBAGE(TEXT)GARBAGE\nGARBAGE\nGARBAGE(\n\tTEXT\n)GARBAGE\nGARBAGE\nGARBAGE(((TEXT)))GARBAGE\nGARBAGE(\n\t(\n\t\t(\n\t\t\tTEXT\n\t\t)\n\t)\n)GARBAGE\nGARBAGE\nGARBAGE(\n\t(TEXT)\n\t(TEXT)\n)GARBAGE\nGARBAGE\nGARBAGE[TEXT]GARBAGE\nGARBAGE\nGARBAGE[\n\tTEXT\n]GARBAGE\nGARBAGE\nGARBAGE[[[TEXT]]]GARBAGE\nGARBAGE[\n\t[\n\t\t[\n\t\t\tTEXT\n\t\t]\n\t]\n]GARBAGE\nGARBAGE\nGARBAGE[\n\t[TEXT]\n\t[TEXT]\n]GARBAGE\nGARBAGE\nGARBAGETEXTGARBAGE\nGARBAGE\nGARBAGE\n\tTEXT\nGARBAGE\nGARBAGE\nGARBAGETEXTGARBAGE\nGARBAGE\n\t\n\t\t\n\t\t\tTEXT\n\t\t\n\t\nGARBAGE\nGARBAGE\nGARBAGE\n\tTEXT\n\tTEXT\nGARBAGE\nGARBAGE' 34 | ); 35 | }); 36 | 37 | it('can perform simple regexp replacements', function() { 38 | expect(balanced.replacements({source: examples.bracketsBasic, open: /\[|\{|\(|/, close: /\]|\}|\)|<\/tag>/, replace: function (source, head, tail) { 39 | return '' + source + ''; 40 | }})).toEqual( 41 | 'GARBAGETEXTGARBAGE\nGARBAGE\nGARBAGE\n\tTEXT\nGARBAGE\nGARBAGE\nGARBAGE{{TEXT}}GARBAGE\nGARBAGE\n\t{\n\t\t{\n\t\t\tTEXT\n\t\t}\n\t}\nGARBAGE\nGARBAGE\nGARBAGE\n\t{TEXT}\n\t{TEXT}\nGARBAGE\nGARBAGE\nGARBAGETEXTGARBAGE\nGARBAGE\nGARBAGE\n\tTEXT\nGARBAGE\nGARBAGE\nGARBAGE((TEXT))GARBAGE\nGARBAGE\n\t(\n\t\t(\n\t\t\tTEXT\n\t\t)\n\t)\nGARBAGE\nGARBAGE\nGARBAGE\n\t(TEXT)\n\t(TEXT)\nGARBAGE\nGARBAGE\nGARBAGETEXTGARBAGE\nGARBAGE\nGARBAGE\n\tTEXT\nGARBAGE\nGARBAGE\nGARBAGE[[TEXT]]GARBAGE\nGARBAGE\n\t[\n\t\t[\n\t\t\tTEXT\n\t\t]\n\t]\nGARBAGE\nGARBAGE\nGARBAGE\n\t[TEXT]\n\t[TEXT]\nGARBAGE\nGARBAGE\nGARBAGETEXTGARBAGE\nGARBAGE\nGARBAGE\n\tTEXT\nGARBAGE\nGARBAGE\nGARBAGETEXTGARBAGE\nGARBAGE\n\t\n\t\t\n\t\t\tTEXT\n\t\t\n\t\nGARBAGE\nGARBAGE\nGARBAGE\n\tTEXT\n\tTEXT\nGARBAGE\nGARBAGE' 42 | ); 43 | }); 44 | 45 | it('can perform head replacements', function () { 46 | expect(balanced.replacements({source: examples.bracketsHead, head: 'head (', open: '(', close: ')', replace: function (source, head, tail) { 47 | return '' + source + ''; 48 | }})).toEqual( 49 | 'GARBAGE \n\t(\n\t\t(\n\t\t\tTEXT\n\t\t)\n\t)\n\thead ()\nGARBAGE\nGARBAGE head2 (\n\t(\n\t\t(\n\t\t\tTEXT\n\t\t)\n\t)\n\thead2 ()\n)GARBAGE\nGARBAGE \n\t(\n\t\t(\n\t\t\tTEXT\n\t\t)\n\t)\n\thead ()\nGARBAGE\nGARBAGE head2 (\n\t(\n\t\t(\n\t\t\tTEXT\n\t\t)\n\t)\n\thead2 ()\n)GARBAGE' 50 | ); 51 | }); 52 | 53 | it('can perform regexp head matches', function () { 54 | expect(balanced.replacements({source: examples.bracketsHead, head: /head\d? \(/, open: '(', close: ')', replace: function (source, head, tail) { 55 | return '' + source + ''; 56 | }})).toEqual( 57 | 'GARBAGE \n\t(\n\t\t(\n\t\t\tTEXT\n\t\t)\n\t)\n\thead ()\nGARBAGE\nGARBAGE \n\t(\n\t\t(\n\t\t\tTEXT\n\t\t)\n\t)\n\thead2 ()\nGARBAGE\nGARBAGE \n\t(\n\t\t(\n\t\t\tTEXT\n\t\t)\n\t)\n\thead ()\nGARBAGE\nGARBAGE \n\t(\n\t\t(\n\t\t\tTEXT\n\t\t)\n\t)\n\thead2 ()\nGARBAGE' 58 | ); 59 | }); 60 | 61 | it('can ignore matches', function () { 62 | var blockComments = balanced.matches({source: examples.comments, open: '/*', close: '*/'}), 63 | singleLineComments = balanced.getRangesForMatch(examples.comments, /^\s*\/\/.+$/gim); 64 | 65 | expect(balanced.replacements({ 66 | source: examples.comments, 67 | open: ['{', '[', '('], 68 | close: ['}', ']', ')'], 69 | ignore: Array.prototype.concat.call([], blockComments, singleLineComments), 70 | replace: function (source, head, tail) { 71 | return '' + source + ''; 72 | } 73 | })).toEqual( 74 | '\n\t{\n\t\t{\n\t\t\tTEXT\n\t\t}\n\t\t{\n\t\t\tTEXT\n\t\t}\n\t}\n\n\ta {\n\n\t}\n\n\ta [\n\n\t]\n\n\ta (\n\n\t)\n\n// {{TEXT}{TEXT}}a{}a[]a()\n/*\n{\n\t{\n\t\t{\n\t\t\tTEXT\n\t\t\n\t\t{\n\t\t\tTEXT\n\t\t}\n\t}\n\n\ta {\n\n\t}\n\n\ta [\n\n\t\n\n\ta (\n\n\t)\n}\n*/' 75 | ); 76 | }); 77 | 78 | it('can ignore matches 2', function () { 79 | var blockComments = balanced.matches({source: examples.comments, open: '/*', close: '*/'}); 80 | 81 | expect(balanced.replacements({ 82 | source: examples.comments, 83 | open: ['{', '[', '('], 84 | close: ['}', ']', ')'], 85 | ignore: blockComments, 86 | replace: function (source, head, tail) { 87 | return '' + source + ''; 88 | } 89 | })).toEqual( 90 | '\n\t{\n\t\t{\n\t\t\tTEXT\n\t\t}\n\t\t{\n\t\t\tTEXT\n\t\t}\n\t}\n\n\ta {\n\n\t}\n\n\ta [\n\n\t]\n\n\ta (\n\n\t)\n\n// {{TEXT}{TEXT}}a{}a[]a()\n/*\n{\n\t{\n\t\t{\n\t\t\tTEXT\n\t\t\n\t\t{\n\t\t\tTEXT\n\t\t}\n\t}\n\n\ta {\n\n\t}\n\n\ta [\n\n\t\n\n\ta (\n\n\t)\n}\n*/' 91 | ); 92 | }); 93 | }); -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | function Balanced (config) { 2 | config = config || {}; 3 | 4 | if (!config.open) throw new Error('Balanced: please provide a "open" property'); 5 | if (!config.close) throw new Error('Balanced: please provide a "close" property'); 6 | 7 | this.balance = config.balance || false; 8 | this.exceptions = config.exceptions || false; 9 | this.caseInsensitive = config.caseInsensitive; 10 | 11 | this.head = config.head || config.open; 12 | this.head = Array.isArray(this.head) ? this.head : [this.head]; 13 | this.open = Array.isArray(config.open) ? config.open : [config.open]; 14 | this.close = Array.isArray(config.close) ? config.close : [config.close]; 15 | 16 | if ( 17 | !Array.isArray(this.head) || 18 | !Array.isArray(this.open) || 19 | !Array.isArray(this.close) || 20 | !(this.head.length === this.open.length && this.open.length === this.close.length) 21 | ) { 22 | throw new Error('Balanced: if you use arrays for a "head,open,close" you must use matching arrays for all options'); 23 | } 24 | 25 | var headRegExp = regExpFromArray(this.head.map(this.regExpFromArrayGroupedMap, this)), 26 | openRegExp = regExpFromArray(this.open.map(this.regExpFromArrayGroupedMap, this)), 27 | closeRegExp = regExpFromArray(this.close.map(this.regExpFromArrayGroupedMap, this)); 28 | 29 | this.regExp = regExpFromArray([headRegExp, openRegExp, closeRegExp], 'g' + (this.caseInsensitive ? 'i' : '')); 30 | this.regExpGroupLength = this.head.length; 31 | } 32 | 33 | Balanced.prototype = { 34 | /** 35 | * helper creating method for running regExpFromArray with one arg and grouped set to true 36 | * 37 | * @param {RegExp/String} value 38 | * @return {RegExp} 39 | */ 40 | regExpFromArrayGroupedMap: function (value) { 41 | return regExpFromArray([value], null, true); 42 | }, 43 | 44 | /** 45 | * Matches contents 46 | * 47 | * @param {String} string 48 | * @param {Array} ignoreRanges 49 | * @return {Array} 50 | */ 51 | matchContentsInBetweenBrackets: function (string, ignoreRanges) { 52 | var regex = new RegExp(this.regExp), 53 | stack = [], 54 | matches = [], 55 | matchedOpening = null, 56 | match, 57 | balanced = true; 58 | 59 | while ((match = regex.exec(string))) { 60 | if (ignoreRanges) { 61 | var ignore = false; 62 | 63 | for (var i = 0; i < ignoreRanges.length; i++) { 64 | if (isIndexInRange(match.index, ignoreRanges[i])) { 65 | ignore = true; 66 | continue; 67 | } 68 | } 69 | 70 | if (ignore) { 71 | continue; 72 | } 73 | } 74 | 75 | var matchResultPosition = match.indexOf(match[0], 1) - 1, 76 | sectionIndex = Math.floor(matchResultPosition / this.regExpGroupLength), 77 | valueIndex = matchResultPosition - (Math.floor(matchResultPosition / this.regExpGroupLength) * this.regExpGroupLength); 78 | 79 | if (!matchedOpening && sectionIndex === 0 && (!this.balance || this.balance && !stack.length)) { 80 | matchedOpening = match; 81 | 82 | if (this.balance) { 83 | stack.push(valueIndex); 84 | } else { 85 | stack = [valueIndex]; 86 | } 87 | } else if (sectionIndex === 1 || sectionIndex === 0) { 88 | stack.push(valueIndex); 89 | } else if (sectionIndex === 2) { 90 | var expectedValueIndex = stack.pop(); 91 | 92 | if (expectedValueIndex === valueIndex) { 93 | if (matchedOpening !== null && stack.length === 0) { 94 | matches.push({ 95 | index: matchedOpening.index, 96 | length: match.index + match[0].length - matchedOpening.index, 97 | head: matchedOpening[0], 98 | tail: match[0] 99 | }); 100 | matchedOpening = null; 101 | } 102 | } else if (this.balance) { 103 | balanced = false; 104 | 105 | if (this.exceptions) { 106 | if (expectedValueIndex === undefined) { 107 | throw errorForStringIndex('Balanced: unexpected close bracket', string, match.index); 108 | } else if (expectedValueIndex !== valueIndex) { 109 | throw errorForStringIndex('Balanced: mismatching close bracket, expected "' + this.close[expectedValueIndex] + '" but found "' + this.close[valueIndex] + '"', string, match.index); 110 | } 111 | } 112 | } 113 | } 114 | } 115 | 116 | if (this.balance) { 117 | if (this.exceptions && !(balanced && stack.length === 0)) { 118 | throw errorForStringIndex('Balanced: expected "' + this.close[stack[0]] + '" close bracket', string, string.length); 119 | } 120 | return balanced && stack.length === 0 ? matches : null; 121 | } else { 122 | return matches; 123 | } 124 | }, 125 | 126 | /** 127 | * Runs replace function against matches, and source. 128 | * 129 | * @param {String} string 130 | * @param {Function} replace 131 | * @param {Array} ignoreRanges 132 | * @return {String} 133 | */ 134 | replaceMatchesInBetweenBrackets: function (string, replace, ignoreRanges) { 135 | var matches = this.matchContentsInBetweenBrackets(string, ignoreRanges); 136 | return replaceMatchesInString(matches, string, replace); 137 | } 138 | }; 139 | 140 | /** 141 | * pads a value with desired padding and length 142 | * 143 | * @param {String/Number} value 144 | * @param {Number} length 145 | * @param {String} padding 146 | * @return {String} 147 | */ 148 | function pad(value, length, padding) { 149 | return (value.toString().length < length) ? pad(padding + value, length) : value; 150 | } 151 | 152 | /** 153 | * creates an error object for the specified index 154 | * 155 | * @param {String} error 156 | * @param {String} string 157 | * @param {Number} index 158 | * @return {Error} 159 | */ 160 | function errorForStringIndex (error, string, index) { 161 | var lines = getRangesForMatch(string.substr(0, index + 1), /^.*\n?$/gim), 162 | allLines = getRangesForMatch(string, /^.*\n?$/gim), 163 | line = lines.length - 1, 164 | lastLineIndex = lines.length ? lines[lines.length - 1].index : 0, 165 | column = index + 1 - lastLineIndex, 166 | message = '', 167 | previewLines = 2, 168 | maxLineNumberWidth = String(lines.length + Math.min(allLines.length - lines.length, previewLines)).length; 169 | 170 | // show current and previous lines 171 | for (var i = previewLines; i >= 0; i--) { 172 | if (line - i >= 0 && allLines[line-i]) { 173 | message += pad(line-i + 1, maxLineNumberWidth, ' ') + ': ' + string.substr(allLines[line-i].index, allLines[line-i].length).replace(/\n/g, '') + '\n'; 174 | } 175 | } 176 | 177 | // add carrot 178 | for (i = 0; i < column - 1 + (maxLineNumberWidth + 2); i++) { 179 | message += '-'; 180 | } 181 | message += '^\n'; 182 | 183 | // show next lines 184 | for (i = 1; i <= previewLines; i++) { 185 | if (line + i >= 0 && allLines[line+i]) { 186 | message += pad(line+i + 1, maxLineNumberWidth, ' ') + ': ' + string.substr(allLines[line+i].index, allLines[line+i].length).replace(/\n/g, '') + '\n'; 187 | } 188 | } 189 | 190 | // replace tabs with spaces 191 | message = message.replace(/\t/g, ' ').replace(/\n$/, ''); 192 | 193 | var errorObject = new Error(error + ' at ' + (line + 1) + ':' + column + '\n\n' + message); 194 | errorObject.line = line + 1; 195 | errorObject.column = column; 196 | errorObject.index = index; 197 | 198 | return errorObject; 199 | } 200 | 201 | /** 202 | * checks if index is inside of range 203 | * 204 | * @param {Number} index 205 | * @param {Object} range 206 | * @return {Boolean} 207 | */ 208 | function isIndexInRange (index, range) { 209 | return index >= range.index && index <= range.index + range.length - 1; 210 | } 211 | 212 | /** 213 | * generates an array of match range objects 214 | * 215 | * @param {String} string 216 | * @param {RegExp} regexp 217 | * @return {Array} 218 | */ 219 | function getRangesForMatch (string, regexp) { 220 | var pattern = new RegExp(regexp), 221 | match, 222 | matches = []; 223 | 224 | if (string) { 225 | while ((match = pattern.exec(string))) { 226 | matches.push({index: match.index, length: match[0].length, match: match[0]}); 227 | 228 | if (!match[0].length) { 229 | pattern.lastIndex++; 230 | } 231 | } 232 | } 233 | 234 | return matches; 235 | } 236 | 237 | /** 238 | * Non-destructive match replacements. 239 | * 240 | * @param {Array} matches 241 | * @param {String} string 242 | * @param {Function} replace 243 | * @return {String} 244 | */ 245 | function replaceMatchesInString (matches, string, replace) { 246 | var offset = 0; 247 | 248 | if (!matches) { 249 | return string; 250 | } 251 | 252 | for (var i = 0; i < matches.length; i++) { 253 | var match = matches[i], 254 | replacement = String(replace(string.substr(match.index + offset + match.head.length, match.length - match.head.length - match.tail.length), match.head, match.tail)); 255 | 256 | string = string.substr(0, match.index + offset) + replacement + string.substr(match.index + offset + match.length, (string.length) - (match.index + offset + match.length)); 257 | 258 | offset += replacement.length - match.length; 259 | } 260 | 261 | return string; 262 | } 263 | 264 | /** 265 | * Escapes a string to be used within a RegExp 266 | * 267 | * @param {String} string 268 | * @return {String} 269 | */ 270 | function escapeRegExp (string) { 271 | return string.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); 272 | } 273 | 274 | /** 275 | * creates an RegExp from an array of string or RegExp 276 | * 277 | * @param {Array} array 278 | * @param {String} flags 279 | * @param {Boolean} grouped 280 | * @return {RegExp} 281 | */ 282 | function regExpFromArray (array, flags, grouped) { 283 | var string = array.map(function (value) { 284 | return value instanceof RegExp ? value.source : escapeRegExp(value); 285 | }, this).join('|'); 286 | 287 | if (grouped) { 288 | string = '(' + string + ')'; 289 | } else { 290 | string = '(?:' + string + ')'; 291 | } 292 | 293 | return new RegExp(string, flags || undefined); 294 | } 295 | 296 | /** 297 | * returns an array of ranges that are not in the without ranges 298 | * 299 | * @param {Array} ranges 300 | * @param {Array} without 301 | * @return {Array} 302 | */ 303 | function rangesWithout (ranges, without) { 304 | return ranges.filter(function (range) { 305 | var ignored = false; 306 | 307 | for (var i = 0; i < without.length; i++) { 308 | if (isIndexInRange(range.index, without[i])) { 309 | ignored = true; 310 | break; 311 | } 312 | } 313 | 314 | return !ignored; 315 | }); 316 | } 317 | 318 | // export generic methods 319 | exports.replaceMatchesInString = replaceMatchesInString; 320 | exports.getRangesForMatch = getRangesForMatch; 321 | exports.isIndexInRange = isIndexInRange; 322 | exports.rangesWithout = rangesWithout; 323 | // exports.escapeRegExp = escapeRegExp; 324 | // exports.regExpFromArray = regExpFromArray; 325 | 326 | // allows you to create a reusable Balance object and use its `replaceMatchesInBetweenBrackets` and `matchContentsInBetweenBrackets` directly 327 | exports.Balanced = Balanced; 328 | 329 | exports.replacements = function (config) { 330 | config = config || {}; 331 | 332 | var balanced = new Balanced({ 333 | head: config.head, 334 | open: config.open, 335 | close: config.close, 336 | balance: config.balance, 337 | exceptions: config.exceptions, 338 | caseInsensitive: config.caseInsensitive 339 | }); 340 | 341 | if (!config.source) throw new Error('Balanced: please provide a "source" property'); 342 | if (typeof config.replace !== 'function') throw new Error('Balanced: please provide a "replace" function'); 343 | 344 | return balanced.replaceMatchesInBetweenBrackets(config.source, config.replace); 345 | }; 346 | exports.matches = function (config) { 347 | var balanced = new Balanced({ 348 | head: config.head, 349 | open: config.open, 350 | close: config.close, 351 | balance: config.balance, 352 | exceptions: config.exceptions, 353 | caseInsensitive: config.caseInsensitive 354 | }); 355 | 356 | if (!config.source) throw new Error('Balanced: please provide a "source" property'); 357 | 358 | return balanced.matchContentsInBetweenBrackets(config.source, config.ignore); 359 | }; -------------------------------------------------------------------------------- /dist/balanced.js: -------------------------------------------------------------------------------- 1 | /** 2 | * balanced.js v0.0.16 3 | */ 4 | var balanced = 5 | /******/ (function(modules) { // webpackBootstrap 6 | /******/ // The module cache 7 | /******/ var installedModules = {}; 8 | /******/ 9 | /******/ // The require function 10 | /******/ function __webpack_require__(moduleId) { 11 | /******/ 12 | /******/ // Check if module is in cache 13 | /******/ if(installedModules[moduleId]) 14 | /******/ return installedModules[moduleId].exports; 15 | /******/ 16 | /******/ // Create a new module (and put it into the cache) 17 | /******/ var module = installedModules[moduleId] = { 18 | /******/ exports: {}, 19 | /******/ id: moduleId, 20 | /******/ loaded: false 21 | /******/ }; 22 | /******/ 23 | /******/ // Execute the module function 24 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); 25 | /******/ 26 | /******/ // Flag the module as loaded 27 | /******/ module.loaded = true; 28 | /******/ 29 | /******/ // Return the exports of the module 30 | /******/ return module.exports; 31 | /******/ } 32 | /******/ 33 | /******/ 34 | /******/ // expose the modules object (__webpack_modules__) 35 | /******/ __webpack_require__.m = modules; 36 | /******/ 37 | /******/ // expose the module cache 38 | /******/ __webpack_require__.c = installedModules; 39 | /******/ 40 | /******/ // __webpack_public_path__ 41 | /******/ __webpack_require__.p = ""; 42 | /******/ 43 | /******/ // Load entry module and return exports 44 | /******/ return __webpack_require__(0); 45 | /******/ }) 46 | /************************************************************************/ 47 | /******/ ([ 48 | /* 0 */ 49 | /***/ function(module, exports, __webpack_require__) { 50 | 51 | function Balanced (config) { 52 | config = config || {}; 53 | 54 | if (!config.open) throw new Error('Balanced: please provide a "open" property'); 55 | if (!config.close) throw new Error('Balanced: please provide a "close" property'); 56 | 57 | this.balance = config.balance || false; 58 | this.exceptions = config.exceptions || false; 59 | this.caseInsensitive = config.caseInsensitive; 60 | 61 | this.head = config.head || config.open; 62 | this.head = Array.isArray(this.head) ? this.head : [this.head]; 63 | this.open = Array.isArray(config.open) ? config.open : [config.open]; 64 | this.close = Array.isArray(config.close) ? config.close : [config.close]; 65 | 66 | if ( 67 | !Array.isArray(this.head) || 68 | !Array.isArray(this.open) || 69 | !Array.isArray(this.close) || 70 | !(this.head.length === this.open.length && this.open.length === this.close.length) 71 | ) { 72 | throw new Error('Balanced: if you use arrays for a "head,open,close" you must use matching arrays for all options'); 73 | } 74 | 75 | var headRegExp = regExpFromArray(this.head.map(this.regExpFromArrayGroupedMap, this)), 76 | openRegExp = regExpFromArray(this.open.map(this.regExpFromArrayGroupedMap, this)), 77 | closeRegExp = regExpFromArray(this.close.map(this.regExpFromArrayGroupedMap, this)); 78 | 79 | this.regExp = regExpFromArray([headRegExp, openRegExp, closeRegExp], 'g' + (this.caseInsensitive ? 'i' : '')); 80 | this.regExpGroupLength = this.head.length; 81 | } 82 | 83 | Balanced.prototype = { 84 | /** 85 | * helper creating method for running regExpFromArray with one arg and grouped set to true 86 | * 87 | * @param {RegExp/String} value 88 | * @return {RegExp} 89 | */ 90 | regExpFromArrayGroupedMap: function (value) { 91 | return regExpFromArray([value], null, true); 92 | }, 93 | 94 | /** 95 | * Matches contents 96 | * 97 | * @param {String} string 98 | * @param {Array} ignoreRanges 99 | * @return {Array} 100 | */ 101 | matchContentsInBetweenBrackets: function (string, ignoreRanges) { 102 | var regex = new RegExp(this.regExp), 103 | stack = [], 104 | matches = [], 105 | matchedOpening = null, 106 | match, 107 | balanced = true; 108 | 109 | while ((match = regex.exec(string))) { 110 | if (ignoreRanges) { 111 | var ignore = false; 112 | 113 | for (var i = 0; i < ignoreRanges.length; i++) { 114 | if (isIndexInRange(match.index, ignoreRanges[i])) { 115 | ignore = true; 116 | continue; 117 | } 118 | } 119 | 120 | if (ignore) { 121 | continue; 122 | } 123 | } 124 | 125 | var matchResultPosition = match.indexOf(match[0], 1) - 1, 126 | sectionIndex = Math.floor(matchResultPosition / this.regExpGroupLength), 127 | valueIndex = matchResultPosition - (Math.floor(matchResultPosition / this.regExpGroupLength) * this.regExpGroupLength); 128 | 129 | if (!matchedOpening && sectionIndex === 0 && (!this.balance || this.balance && !stack.length)) { 130 | matchedOpening = match; 131 | 132 | if (this.balance) { 133 | stack.push(valueIndex); 134 | } else { 135 | stack = [valueIndex]; 136 | } 137 | } else if (sectionIndex === 1 || sectionIndex === 0) { 138 | stack.push(valueIndex); 139 | } else if (sectionIndex === 2) { 140 | var expectedValueIndex = stack.pop(); 141 | 142 | if (expectedValueIndex === valueIndex) { 143 | if (matchedOpening !== null && stack.length === 0) { 144 | matches.push({ 145 | index: matchedOpening.index, 146 | length: match.index + match[0].length - matchedOpening.index, 147 | head: matchedOpening[0], 148 | tail: match[0] 149 | }); 150 | matchedOpening = null; 151 | } 152 | } else if (this.balance) { 153 | balanced = false; 154 | 155 | if (this.exceptions) { 156 | if (expectedValueIndex === undefined) { 157 | throw errorForStringIndex('Balanced: unexpected close bracket', string, match.index); 158 | } else if (expectedValueIndex !== valueIndex) { 159 | throw errorForStringIndex('Balanced: mismatching close bracket, expected "' + this.close[expectedValueIndex] + '" but found "' + this.close[valueIndex] + '"', string, match.index); 160 | } 161 | } 162 | } 163 | } 164 | } 165 | 166 | if (this.balance) { 167 | if (this.exceptions && !(balanced && stack.length === 0)) { 168 | throw errorForStringIndex('Balanced: expected "' + this.close[stack[0]] + '" close bracket', string, string.length); 169 | } 170 | return balanced && stack.length === 0 ? matches : null; 171 | } else { 172 | return matches; 173 | } 174 | }, 175 | 176 | /** 177 | * Runs replace function against matches, and source. 178 | * 179 | * @param {String} string 180 | * @param {Function} replace 181 | * @param {Array} ignoreRanges 182 | * @return {String} 183 | */ 184 | replaceMatchesInBetweenBrackets: function (string, replace, ignoreRanges) { 185 | var matches = this.matchContentsInBetweenBrackets(string, ignoreRanges); 186 | return replaceMatchesInString(matches, string, replace); 187 | } 188 | }; 189 | 190 | /** 191 | * pads a value with desired padding and length 192 | * 193 | * @param {String/Number} value 194 | * @param {Number} length 195 | * @param {String} padding 196 | * @return {String} 197 | */ 198 | function pad(value, length, padding) { 199 | return (value.toString().length < length) ? pad(padding + value, length) : value; 200 | } 201 | 202 | /** 203 | * creates an error object for the specified index 204 | * 205 | * @param {String} error 206 | * @param {String} string 207 | * @param {Number} index 208 | * @return {Error} 209 | */ 210 | function errorForStringIndex (error, string, index) { 211 | var lines = getRangesForMatch(string.substr(0, index + 1), /^.*\n?$/gim), 212 | allLines = getRangesForMatch(string, /^.*\n?$/gim), 213 | line = lines.length - 1, 214 | lastLineIndex = lines.length ? lines[lines.length - 1].index : 0, 215 | column = index + 1 - lastLineIndex, 216 | message = '', 217 | previewLines = 2, 218 | maxLineNumberWidth = String(lines.length + Math.min(allLines.length - lines.length, previewLines)).length; 219 | 220 | // show current and previous lines 221 | for (var i = previewLines; i >= 0; i--) { 222 | if (line - i >= 0 && allLines[line-i]) { 223 | message += pad(line-i + 1, maxLineNumberWidth, ' ') + ': ' + string.substr(allLines[line-i].index, allLines[line-i].length).replace(/\n/g, '') + '\n'; 224 | } 225 | } 226 | 227 | // add carrot 228 | for (i = 0; i < column - 1 + (maxLineNumberWidth + 2); i++) { 229 | message += '-'; 230 | } 231 | message += '^\n'; 232 | 233 | // show next lines 234 | for (i = 1; i <= previewLines; i++) { 235 | if (line + i >= 0 && allLines[line+i]) { 236 | message += pad(line+i + 1, maxLineNumberWidth, ' ') + ': ' + string.substr(allLines[line+i].index, allLines[line+i].length).replace(/\n/g, '') + '\n'; 237 | } 238 | } 239 | 240 | // replace tabs with spaces 241 | message = message.replace(/\t/g, ' ').replace(/\n$/, ''); 242 | 243 | var errorObject = new Error(error + ' at ' + (line + 1) + ':' + column + '\n\n' + message); 244 | errorObject.line = line + 1; 245 | errorObject.column = column; 246 | errorObject.index = index; 247 | 248 | return errorObject; 249 | } 250 | 251 | /** 252 | * checks if index is inside of range 253 | * 254 | * @param {Number} index 255 | * @param {Object} range 256 | * @return {Boolean} 257 | */ 258 | function isIndexInRange (index, range) { 259 | return index >= range.index && index <= range.index + range.length - 1; 260 | } 261 | 262 | /** 263 | * generates an array of match range objects 264 | * 265 | * @param {String} string 266 | * @param {RegExp} regexp 267 | * @return {Array} 268 | */ 269 | function getRangesForMatch (string, regexp) { 270 | var pattern = new RegExp(regexp), 271 | match, 272 | matches = []; 273 | 274 | if (string) { 275 | while ((match = pattern.exec(string))) { 276 | matches.push({index: match.index, length: match[0].length, match: match[0]}); 277 | 278 | if (!match[0].length) { 279 | pattern.lastIndex++; 280 | } 281 | } 282 | } 283 | 284 | return matches; 285 | } 286 | 287 | /** 288 | * Non-destructive match replacements. 289 | * 290 | * @param {Array} matches 291 | * @param {String} string 292 | * @param {Function} replace 293 | * @return {String} 294 | */ 295 | function replaceMatchesInString (matches, string, replace) { 296 | var offset = 0; 297 | 298 | if (!matches) { 299 | return string; 300 | } 301 | 302 | for (var i = 0; i < matches.length; i++) { 303 | var match = matches[i], 304 | replacement = String(replace(string.substr(match.index + offset + match.head.length, match.length - match.head.length - match.tail.length), match.head, match.tail)); 305 | 306 | string = string.substr(0, match.index + offset) + replacement + string.substr(match.index + offset + match.length, (string.length) - (match.index + offset + match.length)); 307 | 308 | offset += replacement.length - match.length; 309 | } 310 | 311 | return string; 312 | } 313 | 314 | /** 315 | * Escapes a string to be used within a RegExp 316 | * 317 | * @param {String} string 318 | * @return {String} 319 | */ 320 | function escapeRegExp (string) { 321 | return string.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); 322 | } 323 | 324 | /** 325 | * creates an RegExp from an array of string or RegExp 326 | * 327 | * @param {Array} array 328 | * @param {String} flags 329 | * @param {Boolean} grouped 330 | * @return {RegExp} 331 | */ 332 | function regExpFromArray (array, flags, grouped) { 333 | var string = array.map(function (value) { 334 | return value instanceof RegExp ? value.source : escapeRegExp(value); 335 | }, this).join('|'); 336 | 337 | if (grouped) { 338 | string = '(' + string + ')'; 339 | } else { 340 | string = '(?:' + string + ')'; 341 | } 342 | 343 | return new RegExp(string, flags || undefined); 344 | } 345 | 346 | /** 347 | * returns an array of ranges that are not in the without ranges 348 | * 349 | * @param {Array} ranges 350 | * @param {Array} without 351 | * @return {Array} 352 | */ 353 | function rangesWithout (ranges, without) { 354 | return ranges.filter(function (range) { 355 | var ignored = false; 356 | 357 | for (var i = 0; i < without.length; i++) { 358 | if (isIndexInRange(range.index, without[i])) { 359 | ignored = true; 360 | break; 361 | } 362 | } 363 | 364 | return !ignored; 365 | }); 366 | } 367 | 368 | // export generic methods 369 | exports.replaceMatchesInString = replaceMatchesInString; 370 | exports.getRangesForMatch = getRangesForMatch; 371 | exports.isIndexInRange = isIndexInRange; 372 | exports.rangesWithout = rangesWithout; 373 | // exports.escapeRegExp = escapeRegExp; 374 | // exports.regExpFromArray = regExpFromArray; 375 | 376 | // allows you to create a reusable Balance object and use its `replaceMatchesInBetweenBrackets` and `matchContentsInBetweenBrackets` directly 377 | exports.Balanced = Balanced; 378 | 379 | exports.replacements = function (config) { 380 | config = config || {}; 381 | 382 | var balanced = new Balanced({ 383 | head: config.head, 384 | open: config.open, 385 | close: config.close, 386 | balance: config.balance, 387 | exceptions: config.exceptions, 388 | caseInsensitive: config.caseInsensitive 389 | }); 390 | 391 | if (!config.source) throw new Error('Balanced: please provide a "source" property'); 392 | if (typeof config.replace !== 'function') throw new Error('Balanced: please provide a "replace" function'); 393 | 394 | return balanced.replaceMatchesInBetweenBrackets(config.source, config.replace); 395 | }; 396 | exports.matches = function (config) { 397 | var balanced = new Balanced({ 398 | head: config.head, 399 | open: config.open, 400 | close: config.close, 401 | balance: config.balance, 402 | exceptions: config.exceptions, 403 | caseInsensitive: config.caseInsensitive 404 | }); 405 | 406 | if (!config.source) throw new Error('Balanced: please provide a "source" property'); 407 | 408 | return balanced.matchContentsInBetweenBrackets(config.source, config.ignore); 409 | }; 410 | 411 | /***/ } 412 | /******/ ]) 413 | //# sourceMappingURL=balanced.js.map -------------------------------------------------------------------------------- /dist/balanced.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["webpack:///webpack/bootstrap ddfde9187d7228244291","webpack:///./index.js"],"names":[],"mappings":";;AAAA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA,uBAAe;AACf;AACA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;;;AAGA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA,wC;;;;;;;ACtCA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA,cAAa,cAAc;AAC3B,cAAa;AACb;AACA;AACA;AACA,GAAE;;AAEF;AACA;AACA;AACA,cAAa,OAAO;AACpB,cAAa,MAAM;AACnB,cAAa;AACb;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA,oBAAmB,yBAAyB;AAC5C;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA,MAAK;AACL;AACA;AACA,KAAI;AACJ;AACA,KAAI;AACJ;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,QAAO;AACP;AACA;AACA,MAAK;AACL;;AAEA;AACA;AACA;AACA,QAAO;AACP;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA,IAAG;AACH;AACA;AACA,GAAE;;AAEF;AACA;AACA;AACA,cAAa,OAAO;AACpB,cAAa,SAAS;AACtB,cAAa,MAAM;AACnB,cAAa;AACb;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA,aAAY,cAAc;AAC1B,aAAY,OAAO;AACnB,aAAY,OAAO;AACnB,aAAY;AACZ;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA,aAAY,OAAO;AACnB,aAAY,OAAO;AACnB,aAAY,OAAO;AACnB,aAAY;AACZ;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA,4BAA2B,QAAQ;AACnC;AACA;AACA;AACA;;AAEA;AACA,aAAY,2CAA2C;AACvD;AACA;AACA;;AAEA;AACA,aAAY,mBAAmB;AAC/B;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA,aAAY,OAAO;AACnB,aAAY,OAAO;AACnB,aAAY;AACZ;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA,aAAY,OAAO;AACnB,aAAY,OAAO;AACnB,aAAY;AACZ;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA,kBAAiB,6DAA6D;;AAE9E;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA,aAAY,MAAM;AAClB,aAAY,OAAO;AACnB,aAAY,SAAS;AACrB,aAAY;AACZ;AACA;AACA;;AAEA;AACA;AACA;;AAEA,iBAAgB,oBAAoB;AACpC;AACA;;AAEA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA,aAAY,OAAO;AACnB,aAAY;AACZ;AACA;AACA,qCAAoC,EAAE;AACtC;;AAEA;AACA;AACA;AACA,aAAY,MAAM;AAClB,aAAY,OAAO;AACnB,aAAY,QAAQ;AACpB,aAAY;AACZ;AACA;AACA;AACA;AACA,GAAE;;AAEF;AACA;AACA,GAAE;AACF;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA,aAAY,MAAM;AAClB,aAAY,MAAM;AAClB,aAAY;AACZ;AACA;AACA;AACA;;AAEA,kBAAiB,oBAAoB;AACrC;AACA;AACA;AACA;AACA;;AAEA;AACA,GAAE;AACF;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,GAAE;;AAEF;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,GAAE;;AAEF;;AAEA;AACA,G","sourcesContent":[" \t// The module cache\n \tvar installedModules = {};\n\n \t// The require function\n \tfunction __webpack_require__(moduleId) {\n\n \t\t// Check if module is in cache\n \t\tif(installedModules[moduleId])\n \t\t\treturn installedModules[moduleId].exports;\n\n \t\t// Create a new module (and put it into the cache)\n \t\tvar module = installedModules[moduleId] = {\n \t\t\texports: {},\n \t\t\tid: moduleId,\n \t\t\tloaded: false\n \t\t};\n\n \t\t// Execute the module function\n \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n \t\t// Flag the module as loaded\n \t\tmodule.loaded = true;\n\n \t\t// Return the exports of the module\n \t\treturn module.exports;\n \t}\n\n\n \t// expose the modules object (__webpack_modules__)\n \t__webpack_require__.m = modules;\n\n \t// expose the module cache\n \t__webpack_require__.c = installedModules;\n\n \t// __webpack_public_path__\n \t__webpack_require__.p = \"\";\n\n \t// Load entry module and return exports\n \treturn __webpack_require__(0);\n\n\n/** WEBPACK FOOTER **\n ** webpack/bootstrap ddfde9187d7228244291\n **/","function Balanced (config) {\n\tconfig = config || {};\n\n\tif (!config.open) throw new Error('Balanced: please provide a \"open\" property');\n\tif (!config.close) throw new Error('Balanced: please provide a \"close\" property');\n\n\tthis.balance = config.balance || false;\n\tthis.exceptions = config.exceptions || false;\n\tthis.caseInsensitive = config.caseInsensitive;\n\n\tthis.head = config.head || config.open;\n\tthis.head = Array.isArray(this.head) ? this.head : [this.head];\n\tthis.open = Array.isArray(config.open) ? config.open : [config.open];\n\tthis.close = Array.isArray(config.close) ? config.close : [config.close];\n\n\tif (\n\t\t!Array.isArray(this.head) ||\n\t\t!Array.isArray(this.open) ||\n\t\t!Array.isArray(this.close) ||\n\t\t!(this.head.length === this.open.length && this.open.length === this.close.length)\n\t) {\n\t\tthrow new Error('Balanced: if you use arrays for a \"head,open,close\" you must use matching arrays for all options');\n\t}\n\n\tvar headRegExp = regExpFromArray(this.head.map(this.regExpFromArrayGroupedMap, this)),\n\t\topenRegExp = regExpFromArray(this.open.map(this.regExpFromArrayGroupedMap, this)),\n\t\tcloseRegExp = regExpFromArray(this.close.map(this.regExpFromArrayGroupedMap, this));\n\n\tthis.regExp = regExpFromArray([headRegExp, openRegExp, closeRegExp], 'g' + (this.caseInsensitive ? 'i' : ''));\n\tthis.regExpGroupLength = this.head.length;\n}\n\nBalanced.prototype = {\n\t/**\n\t * helper creating method for running regExpFromArray with one arg and grouped set to true\n\t *\n\t * @param {RegExp/String} value\n\t * @return {RegExp}\n\t */\n\tregExpFromArrayGroupedMap: function (value) {\n\t\treturn regExpFromArray([value], null, true);\n\t},\n\n\t/**\n\t * Matches contents\n\t *\n\t * @param {String} string\n\t * @param {Array} ignoreRanges\n\t * @return {Array}\n\t */\n\tmatchContentsInBetweenBrackets: function (string, ignoreRanges) {\n\t\tvar regex = new RegExp(this.regExp),\n\t\t\tstack = [],\n\t\t\tmatches = [],\n\t\t\tmatchedOpening = null,\n\t\t\tmatch,\n\t\t\tbalanced = true;\n\n\t\twhile ((match = regex.exec(string))) {\n\t\t\tif (ignoreRanges) {\n\t\t\t\tvar ignore = false;\n\n\t\t\t\tfor (var i = 0; i < ignoreRanges.length; i++) {\n\t\t\t\t\tif (isIndexInRange(match.index, ignoreRanges[i])) {\n\t\t\t\t\t\tignore = true;\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tif (ignore) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tvar matchResultPosition = match.indexOf(match[0], 1) - 1,\n\t\t\t\tsectionIndex = Math.floor(matchResultPosition / this.regExpGroupLength),\n\t\t\t\tvalueIndex = matchResultPosition - (Math.floor(matchResultPosition / this.regExpGroupLength) * this.regExpGroupLength);\n\n\t\t\tif (!matchedOpening && sectionIndex === 0 && (!this.balance || this.balance && !stack.length)) {\n\t\t\t\tmatchedOpening = match;\n\n\t\t\t\tif (this.balance) {\n\t\t\t\t\tstack.push(valueIndex);\n\t\t\t\t} else {\n\t\t\t\t\tstack = [valueIndex];\n\t\t\t\t}\n\t\t\t} else if (sectionIndex === 1 || sectionIndex === 0) {\n\t\t\t\tstack.push(valueIndex);\n\t\t\t} else if (sectionIndex === 2) {\n\t\t\t\tvar expectedValueIndex = stack.pop();\n\n\t\t\t\tif (expectedValueIndex === valueIndex) {\n\t\t\t\t\tif (matchedOpening !== null && stack.length === 0) {\n\t\t\t\t\t\tmatches.push({\n\t\t\t\t\t\t\tindex: matchedOpening.index,\n\t\t\t\t\t\t\tlength: match.index + match[0].length - matchedOpening.index,\n\t\t\t\t\t\t\thead: matchedOpening[0],\n\t\t\t\t\t\t\ttail: match[0]\n\t\t\t\t\t\t});\n\t\t\t\t\t\tmatchedOpening = null;\n\t\t\t\t\t}\n\t\t\t\t} else if (this.balance) {\n\t\t\t\t\tbalanced = false;\n\n\t\t\t\t\tif (this.exceptions) {\n\t\t\t\t\t\tif (expectedValueIndex === undefined) {\n\t\t\t\t\t\t\tthrow errorForStringIndex('Balanced: unexpected close bracket', string, match.index);\n\t\t\t\t\t\t} else if (expectedValueIndex !== valueIndex) {\n\t\t\t\t\t\t\tthrow errorForStringIndex('Balanced: mismatching close bracket, expected \"' + this.close[expectedValueIndex] + '\" but found \"' + this.close[valueIndex] + '\"', string, match.index);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif (this.balance) {\n\t\t\tif (this.exceptions && !(balanced && stack.length === 0)) {\n\t\t\t\tthrow errorForStringIndex('Balanced: expected \"' + this.close[stack[0]] + '\" close bracket', string, string.length);\n\t\t\t}\n\t\t\treturn balanced && stack.length === 0 ? matches : null;\n\t\t} else {\n\t\t\treturn matches;\n\t\t}\n\t},\n\n\t/**\n\t * Runs replace function against matches, and source.\n\t *\n\t * @param {String} string\n\t * @param {Function} replace\n\t * @param {Array} ignoreRanges\n\t * @return {String}\n\t */\n\treplaceMatchesInBetweenBrackets: function (string, replace, ignoreRanges) {\n\t\tvar matches = this.matchContentsInBetweenBrackets(string, ignoreRanges);\n\t\treturn replaceMatchesInString(matches, string, replace);\n\t}\n};\n\n/**\n * pads a value with desired padding and length\n *\n * @param {String/Number} value\n * @param {Number} length\n * @param {String} padding\n * @return {String}\n */\nfunction pad(value, length, padding) {\n return (value.toString().length < length) ? pad(padding + value, length) : value;\n}\n\n/**\n * creates an error object for the specified index\n *\n * @param {String} error\n * @param {String} string\n * @param {Number} index\n * @return {Error}\n */\nfunction errorForStringIndex (error, string, index) {\n\tvar lines = getRangesForMatch(string.substr(0, index + 1), /^.*\\n?$/gim),\n\t\tallLines = getRangesForMatch(string, /^.*\\n?$/gim),\n\t\tline = lines.length - 1,\n\t\tlastLineIndex = lines.length ? lines[lines.length - 1].index : 0,\n\t\tcolumn = index + 1 - lastLineIndex,\n\t\tmessage = '',\n\t\tpreviewLines = 2,\n\t\tmaxLineNumberWidth = String(lines.length + Math.min(allLines.length - lines.length, previewLines)).length;\n\n\t// show current and previous lines\n\tfor (var i = previewLines; i >= 0; i--) {\n\t\tif (line - i >= 0 && allLines[line-i]) {\n\t\t\tmessage += pad(line-i + 1, maxLineNumberWidth, ' ') + ': ' + string.substr(allLines[line-i].index, allLines[line-i].length).replace(/\\n/g, '') + '\\n';\n\t\t}\n\t}\n\n\t// add carrot\n\tfor (i = 0; i < column - 1 + (maxLineNumberWidth + 2); i++) {\n\t\tmessage += '-';\n\t}\n\tmessage += '^\\n';\n\n\t// show next lines\n\tfor (i = 1; i <= previewLines; i++) {\n\t\tif (line + i >= 0 && allLines[line+i]) {\n\t\t\tmessage += pad(line+i + 1, maxLineNumberWidth, ' ') + ': ' + string.substr(allLines[line+i].index, allLines[line+i].length).replace(/\\n/g, '') + '\\n';\n\t\t}\n\t}\n\n\t// replace tabs with spaces\n\tmessage = message.replace(/\\t/g, ' ').replace(/\\n$/, '');\n\n\tvar errorObject = new Error(error + ' at ' + (line + 1) + ':' + column + '\\n\\n' + message);\n\terrorObject.line = line + 1;\n\terrorObject.column = column;\n\terrorObject.index = index;\n\n\treturn errorObject;\n}\n\n/**\n * checks if index is inside of range\n *\n * @param {Number} index\n * @param {Object} range\n * @return {Boolean}\n */\nfunction isIndexInRange (index, range) {\n\treturn index >= range.index && index <= range.index + range.length - 1;\n}\n\n/**\n * generates an array of match range objects\n *\n * @param {String} string\n * @param {RegExp} regexp\n * @return {Array}\n */\nfunction getRangesForMatch (string, regexp) {\n\tvar pattern = new RegExp(regexp),\n\t\tmatch,\n\t\tmatches = [];\n\n\tif (string) {\n\t\twhile ((match = pattern.exec(string))) {\n\t\t\tmatches.push({index: match.index, length: match[0].length, match: match[0]});\n\n\t\t\tif (!match[0].length) {\n\t\t\t\tpattern.lastIndex++;\n\t\t\t}\n\t\t}\n\t}\n\n\treturn matches;\n}\n\n/**\n * Non-destructive match replacements.\n *\n * @param {Array} matches\n * @param {String} string\n * @param {Function} replace\n * @return {String}\n */\nfunction replaceMatchesInString (matches, string, replace) {\n\tvar offset = 0;\n\n\tif (!matches) {\n\t\treturn string;\n\t}\n\n\tfor (var i = 0; i < matches.length; i++) {\n\t\tvar match = matches[i],\n\t\t\treplacement = String(replace(string.substr(match.index + offset + match.head.length, match.length - match.head.length - match.tail.length), match.head, match.tail));\n\n\t\tstring = string.substr(0, match.index + offset) + replacement + string.substr(match.index + offset + match.length, (string.length) - (match.index + offset + match.length));\n\n\t\toffset += replacement.length - match.length;\n\t}\n\n\treturn string;\n}\n\n/**\n * Escapes a string to be used within a RegExp\n *\n * @param {String} string\n * @return {String}\n */\nfunction escapeRegExp (string) {\n return string.replace(/[\\-\\[\\]\\/\\{\\}\\(\\)\\*\\+\\?\\.\\\\\\^\\$\\|]/g, \"\\\\$&\");\n}\n\n/**\n * creates an RegExp from an array of string or RegExp\n *\n * @param {Array} array\n * @param {String} flags\n * @param {Boolean} grouped\n * @return {RegExp}\n */\nfunction regExpFromArray (array, flags, grouped) {\n\tvar string = array.map(function (value) {\n\t\treturn value instanceof RegExp ? value.source : escapeRegExp(value);\n\t}, this).join('|');\n\n\tif (grouped) {\n\t\tstring = '(' + string + ')';\n\t} else {\n\t\tstring = '(?:' + string + ')';\n\t}\n\n\treturn new RegExp(string, flags || undefined);\n}\n\n/**\n * returns an array of ranges that are not in the without ranges\n *\n * @param {Array} ranges\n * @param {Array} without\n * @return {Array}\n */\nfunction rangesWithout (ranges, without) {\n\treturn ranges.filter(function (range) {\n\t\tvar ignored = false;\n\n\t\tfor (var i = 0; i < without.length; i++) {\n\t\t\tif (isIndexInRange(range.index, without[i])) {\n\t\t\t\tignored = true;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\n\t\treturn !ignored;\n\t});\n}\n\n// export generic methods\nexports.replaceMatchesInString = replaceMatchesInString;\nexports.getRangesForMatch = getRangesForMatch;\nexports.isIndexInRange = isIndexInRange;\nexports.rangesWithout = rangesWithout;\n// exports.escapeRegExp = escapeRegExp;\n// exports.regExpFromArray = regExpFromArray;\n\n// allows you to create a reusable Balance object and use its `replaceMatchesInBetweenBrackets` and `matchContentsInBetweenBrackets` directly\nexports.Balanced = Balanced;\n\nexports.replacements = function (config) {\n\tconfig = config || {};\n\n\tvar balanced = new Balanced({\n\t\thead: config.head,\n\t\topen: config.open,\n\t\tclose: config.close,\n\t\tbalance: config.balance,\n\t\texceptions: config.exceptions,\n\t\tcaseInsensitive: config.caseInsensitive\n\t});\n\n\tif (!config.source) throw new Error('Balanced: please provide a \"source\" property');\n\tif (typeof config.replace !== 'function') throw new Error('Balanced: please provide a \"replace\" function');\n\n\treturn balanced.replaceMatchesInBetweenBrackets(config.source, config.replace);\n};\nexports.matches = function (config) {\n\tvar balanced = new Balanced({\n\t\thead: config.head,\n\t\topen: config.open,\n\t\tclose: config.close,\n\t\tbalance: config.balance,\n\t\texceptions: config.exceptions,\n\t\tcaseInsensitive: config.caseInsensitive\n\t});\n\n\tif (!config.source) throw new Error('Balanced: please provide a \"source\" property');\n\n\treturn balanced.matchContentsInBetweenBrackets(config.source, config.ignore);\n};\n\n\n/*****************\n ** WEBPACK FOOTER\n ** ./index.js\n ** module id = 0\n ** module chunks = 0\n **/"],"sourceRoot":"","file":"balanced.js"} --------------------------------------------------------------------------------