├── .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"}
--------------------------------------------------------------------------------