├── .npmignore ├── rocambole.jpg ├── .travis.yml ├── .gitignore ├── test ├── runner.js ├── perf.spec.js ├── moonwalk.spec.js ├── parse.spec.js └── files │ └── crossroads.js ├── package.json ├── CHANGELOG.md ├── .jshintrc ├── README.md └── rocambole.js /.npmignore: -------------------------------------------------------------------------------- 1 | test/ 2 | -------------------------------------------------------------------------------- /rocambole.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/millermedeiros/rocambole/HEAD/rocambole.jpg -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: stable 4 | script: 5 | - "npm test --coverage" 6 | 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .DS_Store? 3 | ._* 4 | .Spotlight-V100 5 | .Trashes 6 | Icon? 7 | ehthumbs.db 8 | Thumbs.db 9 | 10 | ### 11 | 12 | node_modules/ 13 | npm-debug.log 14 | coverage/ 15 | 16 | -------------------------------------------------------------------------------- /test/runner.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | // we run mocha manually otherwise istanbul coverage won't work 4 | // run `npm test --coverage` to generate coverage report 5 | 6 | var Mocha = require('mocha'); 7 | 8 | var opts = { 9 | ui : 'bdd', 10 | reporter : (process.env.REPORTER || 'spec'), 11 | grep : process.env.GREP 12 | }; 13 | 14 | // we use the dot reporter on travis since it works better 15 | if (process.env.TRAVIS) { 16 | opts.reporter = 'dot'; 17 | } 18 | 19 | var m = new Mocha(opts); 20 | 21 | if (process.env.INVERT) { 22 | m.invert(); 23 | } 24 | 25 | m.addFile('test/parse.spec.js'); 26 | m.addFile('test/moonwalk.spec.js'); 27 | m.addFile('test/perf.spec.js'); 28 | 29 | m.run(function(err){ 30 | var exitCode = err? 1 : 0; 31 | process.exit(exitCode); 32 | }); 33 | 34 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rocambole", 3 | "version": "0.8.0", 4 | "description": "Recursively walk and transform EcmaScript AST", 5 | "main": "rocambole.js", 6 | "directories": { 7 | "test": "test" 8 | }, 9 | "scripts": { 10 | "test": "istanbul test test/runner.js" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/millermedeiros/rocambole.git" 15 | }, 16 | "bugs": { 17 | "url": "https://github.com/millermedeiros/rocambole/issues" 18 | }, 19 | "keywords": [ 20 | "ast", 21 | "walk", 22 | "syntax", 23 | "source", 24 | "tree", 25 | "traversal", 26 | "falafel", 27 | "burrito", 28 | "esprima" 29 | ], 30 | "author": "Miller Medeiros ", 31 | "license": "MIT", 32 | "dependencies": { 33 | "esprima": "~4.0.0" 34 | }, 35 | "devDependencies": { 36 | "expect.js": "0.2", 37 | "istanbul": "~0.4.5", 38 | "mocha": "^6.0.2" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /test/perf.spec.js: -------------------------------------------------------------------------------- 1 | /*global describe:false, it:false */ 2 | "use strict"; 3 | 4 | var expect = require('expect.js'); 5 | var rocambole = require('../'); 6 | 7 | var _fs = require('fs'); 8 | 9 | // since it takes a long time to instrument jQuery we avoid doing it multiple 10 | // times and cache the result 11 | var _jqueryAST; 12 | 13 | 14 | describe('performance', function () { 15 | this.timeout(10000); 16 | 17 | describe('rocambole.parse', function () { 18 | it('should be fast :P', function () { 19 | var file = _fs.readFileSync( __dirname +'/files/crossroads.js', 'utf-8'); 20 | var startTime = process.hrtime(); 21 | var ast = rocambole.parse(file); 22 | var diff = process.hrtime(startTime); 23 | expect( diff[0] ).to.be.below( 300 ); 24 | expect( ast.startToken ).not.to.be( undefined ); 25 | }); 26 | 27 | it('should not take forever to instrument jQuery', function () { 28 | var file = _fs.readFileSync( __dirname +'/files/jquery.js', 'utf-8'); 29 | var startTime = process.hrtime(); 30 | var ast = rocambole.parse(file); 31 | var diff = process.hrtime(startTime); 32 | expect( diff[0] ).to.be.below( 10000 ); 33 | expect( ast.startToken ).not.to.be( undefined ); 34 | _jqueryAST = ast; 35 | }); 36 | }); 37 | 38 | describe('rocambole.moonwalk', function () { 39 | it('should not take forever to loop over jQuery nodes', function () { 40 | if (! _jqueryAST) { 41 | var file = _fs.readFileSync( __dirname +'/files/jquery.js', 'utf-8'); 42 | _jqueryAST = rocambole.parse(file); 43 | } 44 | var startTime = process.hrtime(); 45 | var count = 0; 46 | rocambole.moonwalk(_jqueryAST, function(node){ 47 | if (!node) throw new Error('node should not be undefined'); 48 | count += 1; 49 | }); 50 | var diff = process.hrtime(startTime); 51 | expect( diff[0] ).to.be.below( 200 ); 52 | expect( count ).to.be.above( 20000 ); 53 | }); 54 | }); 55 | 56 | }); 57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## v0.8.0 (2019/03/18) 4 | 5 | - update `esprima` to 4.0.1 to handle async/await. 6 | - remote `test/` files from deploy. 7 | 8 | ## v0.7.0 (2015/08/26) 9 | 10 | - Ignore TryStatement's "handlers" property in favor of "handler" 11 | 12 | ## v0.6.0 (2015/03/30) 13 | 14 | - allow custom parser. (#27) 15 | 16 | ## v0.5.1 (2015/03/19) 17 | 18 | - make it compatible with esprima@2.1 (don't loop through the same CatchClause 19 | twice). 20 | 21 | ## v0.5.0 (2015/02/25) 22 | 23 | - updated `esprima` to v2.0 because of ES6 features and to avoid `esprima-fb` 24 | bug related to RegExp. 25 | 26 | ## v0.4.0 (2014/07/14) 27 | 28 | - aliased `rocambole.recursive` as `rocambole.walk` to avoid confusions. 29 | - switched `esprima` dependency to `esprima-fb` because of ES6 features. 30 | 31 | ## v0.3.6 (2014/06/23) 32 | 33 | - really handle sparse arrays (eg. `[,]`), fixes moonwalk. (#15) 34 | 35 | ## v0.3.5 (2014/06/23) 36 | 37 | - handle sparse arrays (eg. `[,]`). (#15) 38 | 39 | ## v0.3.4 (2014/06/23) 40 | 41 | - only add `BlockComment.originalIndent` if `WhiteSpace` is on the start of 42 | a line. 43 | 44 | ## v0.3.3 (2014/04/26) 45 | 46 | - add `toString` to empty programs AST (#16) 47 | 48 | ## v0.3.2 (2014/01/17) 49 | 50 | - exports `BYPASS_RECURSION` (#8) 51 | - fix error if input is empty (#12) 52 | - support anything that implements `toString()` as input (#13) 53 | 54 | ## v0.3.1 (2013/12/15) 55 | 56 | - fix `originalIndent` on `BlockComment` when prev token is not `WhiteSpace`. 57 | 58 | ## v0.3.0 (2013/12/15) 59 | 60 | - add `originalIndent` to `BlockComment` (#11) 61 | 62 | ## v0.2.3 (2013/01/08) 63 | 64 | - improve `rocambole.parse()` performance by 4500%. (#4) 65 | - improve `rocambole.moonwalk()` performance by 11000%. 66 | 67 | ## v0.2.2 (2012/12/19) 68 | 69 | - fix consecutive comments before start of program. (#3) 70 | 71 | ## v0.2.1 (2012/12/13) 72 | 73 | - fix `loc` info on `WhiteSpace` and `LineBreak` tokens. (#2) 74 | 75 | ## v0.2.0 (2012/12/09) 76 | 77 | - Deprecated: 78 | - `token.before()` 79 | - `token.after()` 80 | - `token.remove()` 81 | - `node.getTokens()` 82 | - `ast.nodes` 83 | - avoid recursion over comments. 84 | - fix weird bug on esformatter introduced on v0.1.1 related to `token._ast` 85 | property. 86 | 87 | ## v0.1.1 (2012/12/08) 88 | 89 | - Improve token manipulation methods behavior (`before`, `after`, `remove`) 90 | 91 | ## v0.1.0 (2012/12/06) 92 | 93 | - Initial release 94 | -------------------------------------------------------------------------------- /test/moonwalk.spec.js: -------------------------------------------------------------------------------- 1 | /* globals describe:false, it:false */ 2 | /* jshint node:true */ 3 | "use strict"; 4 | 5 | 6 | var expect = require('expect.js'); 7 | var rocambole = require('../'); 8 | 9 | 10 | describe('moonwalk()', function () { 11 | 12 | it('should generate AST if first arg is a string', function () { 13 | var count = 0; 14 | var ast = rocambole.moonwalk("function fn(x){\n var foo = 'bar';\n if (x) {\n foo += 'baz';\n } else {\n foo += 's';\n }\n return foo;\n}", function(){ 15 | count++; 16 | }); 17 | expect( ast.body ).not.to.be(undefined); 18 | expect( ast.tokens ).not.to.be(undefined); 19 | expect( count ).to.be.greaterThan( 1 ); 20 | }); 21 | 22 | it('should work with existing AST', function () { 23 | var ast = rocambole.parse("function fn(x){\n var foo = 'bar';\n if (x) {\n foo += 'baz';\n } else {\n foo += 's';\n }\n return foo;\n}"); 24 | var count = 0; 25 | rocambole.moonwalk(ast, function(){ 26 | count++; 27 | }); 28 | expect( count ).to.be.greaterThan( 1 ); 29 | }); 30 | 31 | it('should walk AST starting from leaf nodes until it reaches the root', function () { 32 | var prevDepth = Infinity; 33 | var count = 0; 34 | var prevNode; 35 | rocambole.moonwalk("function fn(x){\n var foo = 'bar';\n if (x) {\n foo += 'baz';\n } else {\n foo += 's';\n }\n return foo;\n}", function(node){ 36 | count++; 37 | expect( node.depth <= prevDepth ).to.be( true ); 38 | prevDepth = node.depth; 39 | prevNode = node; 40 | }); 41 | expect( count ).to.be.greaterThan( 1 ); 42 | expect( prevNode.type ).to.be( 'Program' ); // reached root 43 | }); 44 | 45 | it('should skip null nodes', function(){ 46 | var count = 0; 47 | rocambole.moonwalk('[,3,[,4]]', function () { 48 | count++; 49 | }); 50 | expect(count).to.be(6); 51 | }); 52 | 53 | }); 54 | 55 | 56 | describe('recursive()', function () { 57 | 58 | it('should allow breaking the loop', function () { 59 | var ast = rocambole.parse('function fn(x){ return x * 2 }'); 60 | var count_1 = 0; 61 | rocambole.recursive(ast, function(){ 62 | count_1 += 1; 63 | }); 64 | var count_2 = 0; 65 | rocambole.recursive(ast, function(node){ 66 | count_2 += 1; 67 | if (node.type === 'BlockStatement') { 68 | return false; // break 69 | } 70 | }); 71 | expect( count_1 ).to.be( 9 ); 72 | expect( count_2 ).to.be( 5 ); 73 | }); 74 | 75 | it('should be aliased as walk() to avoid confusions', function () { 76 | expect( rocambole.walk ).to.be( rocambole.recursive ); 77 | }); 78 | 79 | }); 80 | 81 | 82 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | // Settings 3 | "passfail" : false, // Stop on first error. 4 | "maxerr" : 500, // Maximum error before stopping. 5 | 6 | 7 | // Predefined globals whom JSHint will ignore. 8 | "browser" : false, // Standard browser globals e.g. `window`, `document`. 9 | "node" : true, 10 | 11 | 12 | // Custom globals. 13 | "predef" : [], 14 | 15 | 16 | // Development. 17 | "debug" : false, // Allow debugger statements e.g. browser breakpoints. 18 | "devel" : false, // Allow developments statements e.g. `console.log();`. 19 | 20 | 21 | // EcmaScript 5. 22 | "es5" : false, // Allow EcmaScript 5 syntax. 23 | "globalstrict" : true, // Allow global "use strict" (also enables 'strict'). 24 | "strict" : true, // Require `use strict` pragma in every file. 25 | 26 | 27 | // The Good Parts. 28 | "asi" : false, // Tolerate Automatic Semicolon Insertion (no semicolons). 29 | "bitwise" : false, // Prohibit bitwise operators (&, |, ^, etc.). 30 | "boss" : true, // Tolerate assignments inside if, for & while. Usually conditions & loops are for comparison, not assignments. 31 | "curly" : false, // Require {} for every new block or scope. 32 | "eqeqeq" : false, // Require triple equals i.e. `===`. 33 | "eqnull" : true, // Tolerate use of `== null`. 34 | "evil" : false, // Tolerate use of `eval`. 35 | "expr" : false, // Tolerate `ExpressionStatement` as Programs. 36 | "forin" : true, // Tolerate `for in` loops without `hasOwnPrototype`. 37 | "immed" : true, // Require immediate invocations to be wrapped in parens e.g. `( function(){}() );` 38 | "latedef" : false, // Prohibit variable use before definition. 39 | "laxbreak" : false, // Tolerate unsafe line breaks e.g. `return [\n] x` without semicolons. 40 | "loopfunc" : false, // Allow functions to be defined within loops. 41 | "noarg" : true, // Prohibit use of `arguments.caller` and `arguments.callee`. 42 | "regexdash" : true, // Tolerate unescaped last dash i.e. `[-...]`. 43 | "regexp" : false, // Prohibit `.` and `[^...]` in regular expressions. 44 | "scripturl" : false, // Tolerate script-targeted URLs. 45 | "shadow" : false, // Allows re-define variables later in code e.g. `var x=1; x=2;`. 46 | "supernew" : false, // Tolerate `new function () { ... };` and `new Object;`. 47 | "undef" : true, // Require all non-global variables be declared before they are used. 48 | "unused" : true, // Check for unused vars 49 | 50 | 51 | // Personal styling prefrences. 52 | "newcap" : true, // Require capitalization of all constructor functions e.g. `new F()`. 53 | "noempty" : true, // Prohipit use of empty blocks. 54 | "nomen" : false, // Prohibit use of initial or trailing underbars in names. 55 | "nonew" : true, // Prohibit use of constructors for side-effects. 56 | "onevar" : false, // Allow only one `var` statement per function. 57 | "plusplus" : false, // Prohibit use of `++` & `--`. 58 | "sub" : false, // Tolerate all forms of subscript notation besides dot notation e.g. `dict['key']` instead of `dict.key`. 59 | "trailing" : true, // Prohibit trailing whitespaces. 60 | "white" : false // Check against strict whitespace and indentation rules. 61 | } 62 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rocambole [![Build Status](https://secure.travis-ci.org/millermedeiros/rocambole.svg?branch=master)](https://travis-ci.org/millermedeiros/rocambole) 2 | 3 | ![rocambole](https://raw.github.com/millermedeiros/rocambole/master/rocambole.jpg) 4 | 5 | Recursively walk and add extra information/helpers to [Esprima / Mozilla 6 | SpiderMonkey Parser API](http://esprima.org/doc/index.html#ast) compatible AST. 7 | 8 | The main difference between other tools is that it also keeps information about 9 | tokens and white spaces and it is meant to be used to transform the tokens and 10 | not the string values itself. 11 | 12 | This library is specially useful for non-destructive AST manipulation. 13 | 14 | 15 | ## Inspiration 16 | 17 | This module was heavily inspired by 18 | [node-falafel](https://github.com/substack/node-falafel) and 19 | [node-burrito](https://github.com/substack/node-burrito) but I needed more 20 | information than what is currently available on falafel (specially about 21 | tokens, empty lines and white spaces) and also needed to do the node traversing 22 | on the opposite order (start from leaf nodes). The amount of changes required 23 | to introduce the new features and the differences on the concept behind the 24 | tool justified a new project. 25 | 26 | It was created mainly to be used on 27 | [esformatter](https://github.com/millermedeiros/esformatter/). 28 | 29 | 30 | 31 | ## Extra Tokens 32 | 33 | Besides all the regular tokens returned by `esprima` we also add a few more 34 | that are important for non-destructive transformations: 35 | 36 | * `WhiteSpace` 37 | - Can store multiple white spaces (tabs are considered white space, line 38 | breaks not). Important if you want to do non-destructive replacements that 39 | are white-space sensitive. 40 | - Multiple subsequent white spaces are treated as a single token. 41 | * `LineBreak` 42 | * `LineComment` 43 | * `BlockComment` 44 | 45 | It's way easier to rebuild the JS string if the tokens already have line breaks 46 | and comments. It's also easier to identify if previous/next/current token is a 47 | LineBreak or Comment (sometimes needed for non-destructive transformations). 48 | 49 | Rocambole structure might change in the future to keep the extraneous tokens 50 | outside the `tokens` array and also add an option to toggle the behavior. 51 | ([issue #7](https://github.com/millermedeiros/rocambole/issues/7)) 52 | 53 | 54 | ## Extra Properties 55 | 56 | Each Node have the following extra properties/methods: 57 | 58 | - `parent` : Node|undefined 59 | - `toString()` : string 60 | - `next` : Node|undefined 61 | - `prev` : Node|undefined 62 | - `depth` : Number 63 | - `startToken` : Token 64 | - `endToken` : Token 65 | 66 | Each token also have: 67 | 68 | - `prev` : Token|undefined 69 | - `next` : Token|undefined 70 | 71 | BlockComment also have: 72 | 73 | - `originalIndent`: String|undefined 74 | 75 | To get a better idea of the generated AST structure try out 76 | [rocambole-visualize](http://piuccio.github.io/rocambole-visualize/). 77 | 78 | 79 | ## Linked List 80 | 81 | You should **treat the tokens as a linked list instead of reading the 82 | `ast.tokens` array** (inserting/removing items from a linked list is very cheap 83 | and won't break the loop). You should grab a reference to the `node.startToken` 84 | and get `token.next` until you find the desired token or reach the end of the 85 | program. To loop between all tokens inside a node you can do like this: 86 | 87 | ```js 88 | var token = node.startToken; 89 | while (token !== node.endToken.next) { 90 | doStuffWithToken(token); 91 | token = token.next; 92 | } 93 | ``` 94 | 95 | The method `toString` loops through all tokens between `node.startToken` and 96 | `node.endToken` grabbing the `token.raw` (used by comments) or `token.value` 97 | properties. To implement a method similar to falafel `update()` you can do 98 | this: 99 | 100 | ```js 101 | function update(node, str){ 102 | var newToken = { 103 | type : 'Custom', // can be anything (not used internally) 104 | value : str 105 | }; 106 | // update linked list references 107 | if ( node.startToken.prev ) { 108 | node.startToken.prev.next = newToken; 109 | newToken.prev = node.startToken.prev; 110 | } 111 | if ( node.endToken.next ) { 112 | node.endToken.next.prev = newToken; 113 | newToken.next = node.endToken.next; 114 | } 115 | node.startToken = node.endToken = newToken; 116 | } 117 | ``` 118 | 119 | 120 | ## Helpers 121 | 122 | I plan to create helpers as separate projects when possible. 123 | 124 | - [rocambole-token](https://github.com/millermedeiros/rocambole-token): helpers for token manipulation/traversal 125 | - [rocambole-node](https://github.com/millermedeiros/rocambole-node): helpers for node manipulation/traversal 126 | - [rocambole-whitespace](https://github.com/millermedeiros/rocambole-whitespace): helpers for whitespace manipulation 127 | - [rocambole-linebreak](https://github.com/millermedeiros/rocambole-linebreak): helpers for line break manipulation 128 | - [rocambole-indent](https://github.com/millermedeiros/rocambole-indent): helpers for indentation 129 | 130 | 131 | ## API 132 | 133 | 134 | ### rocambole.parse(source, [opts]) 135 | 136 | Parses a string and instrument the AST with extra properties/methods. 137 | 138 | ```js 139 | var rocambole = require('rocambole'); 140 | var ast = rocambole.parse(string); 141 | console.log( ast.startToken ); 142 | // to get a string representation of all tokens call toString() 143 | console.log( ast.toString() ); 144 | ``` 145 | 146 | You can pass custom options as the second argument: 147 | 148 | ```js 149 | rocambole.parse(source, { 150 | loc: true, 151 | // custom options are forwarded to the rocambole.parseFn 152 | ecmaFeatures: { 153 | arrowFunctions: true 154 | } 155 | }); 156 | ``` 157 | 158 | **IMPORTANT:** rocambole needs the `range`, `tokens` and `comment` info to 159 | build the token linked list, so these options will always be set to `true`. 160 | 161 | ### rocambole.parseFn:Function 162 | 163 | Allows you to override the function used to parse the program. Defaults to 164 | `esprima.parse`. 165 | 166 | ```js 167 | // espree is compatible with esprima AST so things should work as expected 168 | var espree = require('espree'); 169 | rocambole.parseFn = espree.parse; 170 | rocambole.parseContext = espree; 171 | ``` 172 | 173 | ### rocambole.parseContext:Object 174 | 175 | Sets the context (`this` value) of the `parseFn`. Defaults to `esprima`. 176 | 177 | ### rocambole.parseOptions:Object 178 | 179 | Sets the default options passed to `parseFn`. 180 | 181 | ```js 182 | // default values 183 | rocambole.parseOptions = { 184 | // we need range/tokens/comment info to build the tokens linked list! 185 | range: true, 186 | tokens: true, 187 | comment: true 188 | }; 189 | ``` 190 | 191 | ### rocambole.moonwalk(ast, callback) 192 | 193 | The `moonwalk()` starts at the leaf nodes and go down the tree until it reaches 194 | the root node (`Program`). Each node will be traversed only once. 195 | 196 | ```js 197 | rocambole.moonwalk(ast, function(node){ 198 | if (node.type == 'ArrayExpression'){ 199 | console.log( node.depth +': '+ node.toString() ); 200 | } 201 | }); 202 | ``` 203 | 204 | Traverse order: 205 | 206 | ``` 207 | Program [#18] 208 | `-FunctionDeclaration [#16] 209 | |-BlockStatement [#14] 210 | | |-IfStatement [#12] 211 | | | |-BynaryExpression [#9] 212 | | | | |-Identifier [#4] 213 | | | | `-Literal [#5] 214 | | | `-BlockStatement [#10] 215 | | | `-ExpressionStatement [#6] 216 | | | `-AssignmentExpression [#3] 217 | | | |-Identifier [#1 walk starts here] 218 | | | `-Literal [#2] 219 | | `-VariableDeclaration [#13] 220 | | `-VariableDeclarator [#11] 221 | | |-Identifier [#7] 222 | | `-Literal [#8] 223 | `-ReturnStatement [#17] 224 | `-Identifier [#15] 225 | ``` 226 | 227 | This behavior is very different from node-falafel and node-burrito. 228 | 229 | 230 | ### rocambole.walk(ast, callback) 231 | 232 | It loops through all nodes on the AST starting from the root node (`Program`), 233 | similar to `node-falafel`. 234 | 235 | ```js 236 | rocambole.walk(ast, function(node){ 237 | console.log(node.type); 238 | }); 239 | ``` 240 | 241 | 242 | ## Popular Alternatives 243 | 244 | - [burrito](https://github.com/substack/node-burrito) 245 | - [falafel](https://github.com/substack/node-falafel) 246 | 247 | 248 | 249 | ## Unit Tests 250 | 251 | Besides the regular unit tests we also use 252 | [istanbul](https://github.com/yahoo/istanbul) to generate code coverage 253 | reports, tests should have at least 95% code coverage for statements, branches 254 | and lines and 100% code coverage for functions or travis build will fail. 255 | 256 | We do not run the coverage test at each call since it slows down the 257 | performnace of the tests and it also makes it harder to see the test results. 258 | To execute tests and generate coverage report call `npm test --coverage`, for 259 | regular tests just do `npm test`. 260 | 261 | Coverage reports are not committed to the repository since they will change at 262 | each `npm test --coverage` call. 263 | 264 | 265 | 266 | ## License 267 | 268 | MIT 269 | 270 | -------------------------------------------------------------------------------- /rocambole.js: -------------------------------------------------------------------------------- 1 | /*jshint node:true */ 2 | "use strict"; 3 | 4 | 5 | var esprima = require('esprima'); 6 | 7 | 8 | 9 | // --- 10 | 11 | // we expose the flags so other tools can tweak the values (#8) 12 | exports.BYPASS_RECURSION = { 13 | root : true, 14 | comments : true, 15 | tokens : true, 16 | 17 | loc : true, 18 | range : true, 19 | 20 | parent : true, 21 | next : true, 22 | prev : true, 23 | 24 | // esprima@2.1 introduces a "handler" property on TryStatement in addition to 25 | // "handlers", which contains the same node, so we would 26 | // loop the same node twice (see jquery/esprima/issues/1031 and #264)` 27 | // 28 | // Instead, ignore the handlers list in favor of the standardized "handler" 29 | // property: https://github.com/eslint/eslint/issues/1930 30 | handlers : true, 31 | 32 | // IMPORTANT! "value" can't be bypassed since it is used by object 33 | // expression 34 | type : true, 35 | raw : true, 36 | 37 | startToken : true, 38 | endToken : true 39 | }; 40 | 41 | 42 | // --- 43 | 44 | var _addLocInfo; 45 | 46 | // --- 47 | 48 | exports.parseFn = esprima.parse; 49 | exports.parseContext = esprima; 50 | // we need range/tokens/comment info to build the tokens linked list! 51 | exports.parseOptions = { 52 | range: true, 53 | tokens: true, 54 | comment: true 55 | }; 56 | 57 | // parse string and return an augmented AST 58 | exports.parse = function parse(source, opts){ 59 | opts = opts || {}; 60 | _addLocInfo = Boolean(opts.loc); 61 | source = source.toString(); 62 | 63 | Object.keys(exports.parseOptions).forEach(function(key) { 64 | if (!(key in opts)) { 65 | opts[key] = exports.parseOptions[key]; 66 | } 67 | }); 68 | 69 | var ast = exports.parseFn.call(exports.parseContext, source, opts); 70 | 71 | // we augment just root node since program is "empty" 72 | // can't check `ast.body.length` because it might contain just comments 73 | if (!ast.tokens.length && !ast.comments.length) { 74 | ast.depth = 0; 75 | ast.startToken = ast.endToken = null; 76 | ast.toString = _nodeProto.toString; 77 | return ast; 78 | } 79 | 80 | instrumentTokens(ast, source); 81 | 82 | // update program range since it doesn't include white spaces and comments 83 | // before/after the program body by default 84 | var lastToken = ast.tokens[ast.tokens.length - 1]; 85 | ast.range[0] = ast.tokens[0].range[0]; 86 | ast.range[1] = lastToken.range[1]; 87 | if (_addLocInfo) { 88 | ast.loc.start.line = 0; 89 | ast.loc.start.column = 0; 90 | ast.loc.end.line = lastToken.loc.end.line; 91 | ast.loc.end.column = lastToken.loc.end.column; 92 | } 93 | 94 | var toString = _nodeProto.toString; 95 | var instrumentNodes = function(node, parent, prev, next){ 96 | 97 | node.parent = parent; 98 | node.prev = prev; 99 | node.next = next; 100 | node.depth = parent? parent.depth + 1 : 0; // used later for moonwalk 101 | 102 | node.toString = toString; 103 | 104 | // we do not add nextToken and prevToken to avoid updating even more 105 | // references during each remove/before/after you can grab the 106 | // prev/next token by simply accesing the startToken.prev and 107 | // endToken.next 108 | var prevToken = prev? prev.endToken : (parent? parent.startToken : null); 109 | var nextToken = parent? parent.endToken : null; 110 | node.startToken = prevToken? getNodeStartToken(prevToken, node.range) : ast.tokens[0]; 111 | node.endToken = nextToken? getNodeEndToken(nextToken, node.range) : ast.tokens[ast.tokens.length - 1]; 112 | }; 113 | recursiveWalk(ast, instrumentNodes); 114 | 115 | return ast; 116 | }; 117 | 118 | 119 | var _nodeProto = {}; 120 | 121 | // get the node string 122 | _nodeProto.toString = function(){ 123 | var str = ''; 124 | var token = this.startToken; 125 | if (!token) return str; 126 | do { 127 | str += ('raw' in token)? token.raw : token.value; 128 | token = token.next; 129 | } while (token && token !== this.endToken.next); 130 | return str; 131 | }; 132 | 133 | 134 | function getNodeStartToken(token, range){ 135 | var startRange = range[0]; 136 | while (token){ 137 | if (token.range[0] >= startRange) { 138 | return token; 139 | } 140 | token = token.next; 141 | } 142 | } 143 | 144 | function getNodeEndToken(token, range){ 145 | var endRange = range[1]; 146 | while (token){ 147 | if (token.range[1] <= endRange) { 148 | return token; 149 | } 150 | token = token.prev; 151 | } 152 | } 153 | 154 | 155 | 156 | function getPrevToken(tokens, range){ 157 | var result, token, 158 | startRange = range[0], 159 | n = tokens.length; 160 | while (n--) { 161 | token = tokens[n]; 162 | if (token.range[1] <= startRange) { 163 | result = token; 164 | break; 165 | } 166 | } 167 | return result; 168 | } 169 | 170 | 171 | 172 | 173 | 174 | 175 | function instrumentTokens(ast, source){ 176 | 177 | var tokens = ast.tokens; 178 | 179 | 180 | // --- inject comments into tokens list 181 | var comments = ast.comments; 182 | var comment, 183 | q = -1, 184 | nComments = comments.length; 185 | 186 | while (++q < nComments) { 187 | comment = comments[q]; 188 | // we edit it in place since it is faster, will also affect 189 | comment.raw = comment.type === 'Block'? '/*'+ comment.value +'*/' : '//'+ comment.value; 190 | comment.type += 'Comment'; 191 | 192 | var prevToken = getPrevToken(tokens, comment.range); 193 | var prevIndex = prevToken? tokens.indexOf(prevToken) : -1; 194 | tokens.splice(prevIndex + 1, 0, comment); 195 | } 196 | 197 | 198 | // --- inject white spaces and line breaks 199 | 200 | // we create a new array since it's simpler than using splice, it will 201 | // also avoid mistakes 202 | var result = []; 203 | 204 | // insert white spaces before start of program 205 | var wsTokens; 206 | var firstToken = ast.tokens[0]; 207 | var raw; 208 | if ( firstToken.range[0] ) { 209 | raw = source.substring(0, firstToken.range[0]); 210 | result = result.concat( getWhiteSpaceTokens(raw, null) ); 211 | } 212 | 213 | // insert white spaces between regular tokens 214 | // faster than forEach and reduce lookups 215 | var i = -1, 216 | nTokens = tokens.length, 217 | token, prev; 218 | var k, nWs; 219 | while (++i < nTokens) { 220 | token = tokens[i]; 221 | if (i) { 222 | if (prev.range[1] < token.range[0]) { 223 | wsTokens = getWhiteSpaceTokens(source.substring(prev.range[1], token.range[0]), prev); 224 | // faster than concat or push.apply 225 | k = -1; 226 | nWs = wsTokens.length; 227 | while (++k < nWs) { 228 | result.push(wsTokens[k]); 229 | } 230 | } 231 | } 232 | result.push(token); 233 | prev = token; 234 | } 235 | 236 | // insert white spaces after end of program 237 | var lastToken = ast.tokens[ast.tokens.length - 1]; 238 | if (lastToken.range[1] < source.length) { 239 | wsTokens = getWhiteSpaceTokens(source.substring(lastToken.range[1], source.length), lastToken); 240 | k = -1; 241 | nWs = wsTokens.length; 242 | while (++k < nWs) { 243 | result.push(wsTokens[k]); 244 | } 245 | } 246 | 247 | // --- instrument tokens 248 | 249 | // need to come afterwards since we add line breaks and comments 250 | var n; 251 | for (i = 0, n = result.length, token; i < n; i++) { 252 | token = result[i]; 253 | token.prev = i? result[i - 1] : undefined; 254 | token.next = result[i + 1]; 255 | token.root = ast; // used internally 256 | // original indent is very important for block comments since some 257 | // transformations require manipulation of raw comment value 258 | if ( 259 | token.type === 'BlockComment' && 260 | token.prev && token.prev.type === 'WhiteSpace' && 261 | (!token.prev.prev || (token.prev.prev.type === 'LineBreak')) 262 | ) { 263 | token.originalIndent = token.prev.value; 264 | } 265 | } 266 | 267 | ast.tokens = result; 268 | } 269 | 270 | 271 | function getWhiteSpaceTokens(raw, prev){ 272 | var whiteSpaces = getWhiteSpaces(raw); 273 | 274 | var startRange = prev? prev.range[1] : 0; 275 | // line starts at 1 !!! 276 | var startLine, startColumn; 277 | if (_addLocInfo) { 278 | startLine = prev? prev.loc.end.line : 1; 279 | startColumn = prev? prev.loc.end.column : 0; 280 | } 281 | 282 | var tokens = []; 283 | for (var i = 0, n = whiteSpaces.length, value; i < n; i++){ 284 | value = whiteSpaces[i]; 285 | 286 | var wsToken = { value : value }; 287 | var isBr = '\r\n'.indexOf(value) >= 0; 288 | wsToken.type = isBr? 'LineBreak' : 'WhiteSpace'; 289 | wsToken.range = [startRange, startRange + value.length]; 290 | 291 | if (_addLocInfo) { 292 | wsToken.loc = { 293 | start : { 294 | line : startLine, 295 | column : startColumn 296 | }, 297 | end : { 298 | line : startLine, // yes, br starts and end on same line 299 | column : startColumn + value.length 300 | } 301 | }; 302 | 303 | if (isBr) { 304 | // next token after a
always starts at zero and on next line 305 | startLine = wsToken.loc.end.line + 1; 306 | startColumn = 0; 307 | } else { 308 | startLine = wsToken.loc.end.line; 309 | startColumn = wsToken.loc.end.column; 310 | } 311 | } 312 | 313 | startRange += value.length; 314 | tokens.push(wsToken); 315 | } 316 | 317 | return tokens; 318 | } 319 | 320 | 321 | function getWhiteSpaces(source) { 322 | var result = []; 323 | var whiteSpaces = source.split(''); 324 | var buf = ''; 325 | for (var value, i = 0, nSpaces = whiteSpaces.length; i < nSpaces; i++) { 326 | value = whiteSpaces[i]; 327 | switch(value){ 328 | case '\n': 329 | if (buf === '\r') { 330 | // DOS line break 331 | result.push(buf + value); 332 | } else { 333 | if (buf) { 334 | result.push(buf); 335 | } 336 | // unix break 337 | result.push(value); 338 | } 339 | buf = ''; 340 | break; 341 | case '\r': 342 | // might be multiple consecutive Mac breaks 343 | if (buf) { 344 | result.push(buf); 345 | } 346 | buf = value; 347 | break; 348 | default: 349 | if (buf === '\r') { 350 | result.push(buf); 351 | buf = value; 352 | } else { 353 | // group multiple white spaces into same token 354 | buf += value; 355 | } 356 | } 357 | } 358 | if (buf) { 359 | result.push(buf); 360 | } 361 | return result; 362 | } 363 | 364 | 365 | 366 | exports.walk = exports.recursive = recursiveWalk; 367 | 368 | // heavily inspired by node-falafel 369 | // walk nodes recursively starting from root 370 | function recursiveWalk(node, fn, parent, prev, next){ 371 | // sparse arrays might have `null` elements, so we skip those for now 372 | // see issue #15 373 | if ( !node || fn(node, parent, prev, next) === false ) { 374 | return; // stop recursion 375 | } 376 | 377 | // faster than for in 378 | var keys = Object.keys(node), 379 | child, key; 380 | 381 | for (var i = 0, nKeys = keys.length; i < nKeys; i++) { 382 | 383 | key = keys[i]; 384 | child = node[key]; 385 | 386 | // only need to recurse real nodes and arrays 387 | // ps: typeof null == 'object' 388 | if (!child || typeof child !== 'object' || exports.BYPASS_RECURSION[key]) { 389 | continue; 390 | } 391 | 392 | // inception 393 | if (typeof child.type === 'string') { // faster than boolean coercion 394 | recursiveWalk(child, fn, node); 395 | } else if ( typeof child.length === 'number' ) { // faster than Array.isArray and boolean coercion 396 | // faster than forEach 397 | for (var k = 0, nChilds = child.length; k < nChilds; k++) { 398 | recursiveWalk(child[k], fn, node, (k? child[k - 1] : undefined), child[k + 1] ); 399 | } 400 | } 401 | 402 | } 403 | 404 | } 405 | 406 | 407 | 408 | // walk AST starting from leaf nodes 409 | exports.moonwalk = function moonwalk(ast, fn){ 410 | if (typeof ast === 'string') { 411 | ast = exports.parse(ast); 412 | } 413 | 414 | // we create a separate array for each depth and than we flatten it to 415 | // boost performance, way faster than doing an insertion sort 416 | var swap = []; 417 | recursiveWalk(ast, function(node){ 418 | if (! swap[node.depth]) { 419 | swap[node.depth] = []; 420 | } 421 | swap[node.depth].push(node); 422 | }); 423 | 424 | var nodes = []; 425 | var nDepths = swap.length, cur; 426 | while (cur = swap[--nDepths]) { 427 | for (var i = 0, n = cur.length; i < n; i++) { 428 | nodes.push(cur[i]); 429 | } 430 | } 431 | 432 | nodes.forEach(fn); 433 | return ast; 434 | }; 435 | 436 | -------------------------------------------------------------------------------- /test/parse.spec.js: -------------------------------------------------------------------------------- 1 | /*global describe:false, it:false, beforeEach:false, afterEach:false */ 2 | "use strict"; 3 | 4 | var expect = require('expect.js'); 5 | var rocambole = require('../'); 6 | 7 | 8 | describe('parse', function () { 9 | 10 | it('should parse string and return AST', function () { 11 | var ast = rocambole.parse('(function(){ return 123 })'); 12 | expect( ast.type ).to.equal( 'Program' ); 13 | expect( ast.body[0].type ).to.equal( 'ExpressionStatement' ); 14 | }); 15 | 16 | 17 | it('should include tokens before and after "program" end', function () { 18 | var ast = rocambole.parse('//foo\n(function(){ return 123 })\n//bar\n'); 19 | expect( ast.startToken.value ).to.equal( 'foo' ); 20 | expect( ast.endToken.value ).to.equal( '\n' ); 21 | ast = rocambole.parse('\n//foo\n(function(){ return 123 })\n//dolor'); 22 | expect( ast.startToken.value ).to.equal( '\n' ); 23 | expect( ast.endToken.value ).to.equal( 'dolor' ); 24 | }); 25 | 26 | 27 | it('should work with any kind of line breaks & spaces', function () { 28 | var ast = rocambole.parse('\nvar n\r\n=\n10;\r\r \t\t \n', {loc : true}); 29 | 30 | var br_1 = ast.startToken; 31 | expect( br_1.type ).to.be( 'LineBreak' ); 32 | expect( br_1.value ).to.be( '\n' ); 33 | expect( br_1.range ).to.eql( [0, 1] ); 34 | expect( br_1.loc ).to.eql({ 35 | start : { 36 | line : 1, 37 | column : 0 38 | }, 39 | end : { 40 | line : 1, 41 | column : 1 42 | } 43 | }); 44 | 45 | var ws_1 = ast.startToken.next.next; 46 | expect( ws_1.type ).to.be( 'WhiteSpace' ); 47 | expect( ws_1.value ).to.be( ' ' ); 48 | 49 | var br_2 = br_1.next.next.next.next; 50 | expect( br_2.type ).to.be( 'LineBreak' ); 51 | expect( br_2.value ).to.be( '\r\n' ); 52 | expect( br_2.range ).to.eql( [6, 8] ); 53 | expect( br_2.loc ).to.eql({ 54 | start : { 55 | line : 2, 56 | column : 5 57 | }, 58 | end : { 59 | line : 2, 60 | column : 7 61 | } 62 | }); 63 | 64 | // it's important to notice that esprima doesn't parse "\r" as line 65 | // break, so if it is not at EOF it will give conflicting "loc" info. 66 | var br_6 = ast.endToken; 67 | expect( br_6.type ).to.be( 'LineBreak' ); 68 | expect( br_6.value ).to.be( '\n' ); 69 | expect( br_6.range ).to.eql( [21, 22] ); 70 | expect( br_6.loc ).to.eql({ 71 | start : { 72 | line : 6, 73 | column : 6 74 | }, 75 | end : { 76 | line : 6, 77 | column : 7 78 | } 79 | }); 80 | 81 | var ws_2 = ast.endToken.prev; 82 | expect( ws_2.type ).to.be( 'WhiteSpace' ); 83 | expect( ws_2.value ).to.be( ' \t\t ' ); 84 | expect( ws_2.range ).to.eql( [15, 21] ); 85 | expect( ws_2.loc ).to.eql({ 86 | start : { 87 | line : 6, 88 | column : 0 89 | }, 90 | end : { 91 | line : 6, 92 | column : 6 93 | } 94 | }); 95 | 96 | var br_5 = ws_2.prev; 97 | expect( br_5.type ).to.be( 'LineBreak' ); 98 | expect( br_5.value ).to.be( '\r' ); 99 | 100 | var br_4 = br_5.prev; 101 | expect( br_4.type ).to.be( 'LineBreak' ); 102 | expect( br_4.value ).to.be( '\r' ); 103 | }); 104 | 105 | 106 | it('should not include any char that isn\'t a white space on a WhiteSpace token [issue #3]', function () { 107 | var ast = rocambole.parse("\n/* foo */\n/* bar */\nfunction foo(){\n var bar = 'baz';\n\n //foo\n //bar\n\n var lorem = 'ipsum';\n return bar + lorem;\n}"); 108 | var tk = ast.startToken; 109 | var nComments = 0; 110 | while (tk) { 111 | if (tk.type === 'WhiteSpace') { 112 | expect( tk.value ).to.match( /^[\s\t]+$/ ); 113 | } else if (tk.type === 'LineBreak') { 114 | expect( tk.value ).to.equal( '\n' ); 115 | } else if (tk.type === 'LineComment') { 116 | expect( tk.raw ).to.match( /^\/\/\w{3}$/ ); 117 | nComments++; 118 | } else if (tk.type === 'BlockComment') { 119 | expect( tk.raw ).to.match( /^\/\* \w{3} \*\/$/ ); 120 | nComments++; 121 | } 122 | tk = tk.next; 123 | } 124 | expect( nComments ).to.be( 4 ); 125 | }); 126 | 127 | 128 | it('should instrument object expression "value" node', function () { 129 | // this was a bug introduced while trying to improve performance 130 | var ast = rocambole.parse('amet(123, a, {flag : true});'); 131 | var exp = ast.body[0].expression; 132 | expect( exp.startToken ).not.to.be(undefined); 133 | expect( exp.callee.startToken ).not.to.be(undefined); 134 | expect( exp['arguments'][0].startToken ).not.to.be(undefined); 135 | expect( exp['arguments'][1].startToken ).not.to.be(undefined); 136 | expect( exp['arguments'][2].startToken ).not.to.be(undefined); 137 | expect( exp['arguments'][2].properties[0].startToken ).not.to.be(undefined); 138 | expect( exp['arguments'][2].properties[0].key.startToken ).not.to.be(undefined); 139 | expect( exp['arguments'][2].properties[0].value.startToken ).not.to.be(undefined); 140 | }); 141 | 142 | 143 | 144 | describe('Node', function () { 145 | 146 | var ast, program, expressionStatement, 147 | fnExpression, block, returnStatement; 148 | 149 | beforeEach(function(){ 150 | ast = rocambole.parse('/* block */\n(function(){\n return 123; // line\n})'); 151 | program = ast; 152 | expressionStatement = ast.body[0]; 153 | fnExpression = expressionStatement.expression; 154 | block = fnExpression.body; 155 | returnStatement = block.body[0]; 156 | }); 157 | 158 | describe('node.parent', function () { 159 | it('should add reference to parent node', function () { 160 | expect( program.parent ).to.equal( undefined ); 161 | expect( expressionStatement.parent ).to.equal( program ); 162 | expect( fnExpression.parent ).to.equal( expressionStatement ); 163 | expect( block.parent ).to.equal( fnExpression ); 164 | }); 165 | }); 166 | 167 | 168 | describe('node.toString()', function(){ 169 | it('should return the node source', function () { 170 | expect( returnStatement.type ).to.equal( 'ReturnStatement' ); 171 | expect( returnStatement.toString() ).to.equal( 'return 123;' ); 172 | }); 173 | it('should use raw value of comments', function () { 174 | expect( block.toString() ).to.equal( '{\n return 123; // line\n}' ); 175 | }); 176 | it('should use raw value of comments', function () { 177 | expect( ast.toString() ).to.equal( '/* block */\n(function(){\n return 123; // line\n})' ); 178 | }); 179 | }); 180 | 181 | 182 | describe('depth', function () { 183 | it('should add depth property to nodes', function () { 184 | expect( program.depth ).to.equal( 0 ); 185 | expect( expressionStatement.depth ).to.equal( 1 ); 186 | expect( fnExpression.depth ).to.equal( 2 ); 187 | expect( block.depth ).to.equal( 3 ); 188 | expect( returnStatement.depth ).to.equal( 4 ); 189 | }); 190 | }); 191 | 192 | 193 | describe('node.endToken', function () { 194 | it('should return last token inside node', function () { 195 | expect( program.endToken.value ).to.equal( ')' ); 196 | expect( expressionStatement.endToken.value ).to.equal( ')' ); 197 | expect( fnExpression.endToken.value ).to.equal( '}' ); 198 | expect( block.endToken.value ).to.equal( '}' ); 199 | expect( returnStatement.endToken.value ).to.equal( ';' ); 200 | }); 201 | 202 | it('should capture end token properly', function () { 203 | var ast = rocambole.parse('[1,2,[3,4,[5,6,[7,8,9]]]];'); 204 | var exp = ast.body[0].expression; 205 | expect( exp.endToken.value ).to.equal( ']' ); 206 | expect( exp.elements[0].value ).to.equal( 1 ); 207 | expect( exp.elements[0].startToken.value ).to.equal( '1' ); 208 | expect( exp.elements[0].endToken.value ).to.equal( '1' ); 209 | }); 210 | }); 211 | 212 | 213 | describe('node.startToken', function () { 214 | it('should return first token inside node', function () { 215 | expect( program.startToken.value ).to.equal( ' block ' ); 216 | expect( expressionStatement.startToken.value ).to.equal( '(' ); 217 | expect( fnExpression.startToken.value ).to.equal( 'function' ); 218 | expect( block.startToken.value ).to.equal( '{' ); 219 | expect( returnStatement.startToken.value ).to.equal( 'return' ); 220 | }); 221 | }); 222 | 223 | 224 | describe('Node.next & Node.prev', function () { 225 | it('should return reference to previous and next nodes', function () { 226 | var ast = rocambole.parse("\n/* foo */\n/* bar */\nfunction foo(){\n var bar = 'baz';\n var lorem = 'ipsum';\n return bar + lorem;\n}"); 227 | var block = ast.body[0].body.body; 228 | var firstNode = block[0]; 229 | var secondNode = block[1]; 230 | var lastNode = block[2]; 231 | expect( firstNode.prev ).to.equal( undefined ); 232 | expect( firstNode.next ).to.equal( secondNode ); 233 | expect( secondNode.prev ).to.equal( firstNode ); 234 | expect( secondNode.next ).to.equal( lastNode ); 235 | expect( lastNode.prev ).to.equal( secondNode ); 236 | expect( lastNode.next ).to.equal( undefined ); 237 | }); 238 | }); 239 | 240 | }); 241 | 242 | 243 | describe('Token', function () { 244 | 245 | it('should instrument tokens', function () { 246 | var ast = rocambole.parse('function foo(){ return "bar"; }'); 247 | var tokens = ast.tokens; 248 | 249 | expect( tokens[0].prev ).to.be(undefined); 250 | expect( tokens[0].next ).to.be( tokens[1] ); 251 | expect( tokens[1].prev ).to.be( tokens[0] ); 252 | expect( tokens[1].next ).to.be( tokens[2] ); 253 | }); 254 | 255 | it('should add range and loc info to comment tokens', function () { 256 | var ast = rocambole.parse('\n/* foo\n bar\n*/\nfunction foo(){ return "bar"; }\n// end', {loc:true}); 257 | var blockComment = ast.startToken.next; 258 | expect( blockComment.range ).to.eql( [1, 16] ); 259 | expect( blockComment.loc ).to.eql({ 260 | start : { 261 | line : 2, 262 | column : 0 263 | }, 264 | end : { 265 | line : 4, 266 | column : 2 267 | } 268 | }); 269 | var lineComment = ast.endToken; 270 | expect( lineComment.range ).to.eql( [49, 55] ); 271 | expect( lineComment.loc ).to.eql({ 272 | start : { 273 | line : 6, 274 | column : 0 275 | }, 276 | end : { 277 | line : 6, 278 | column : 6 279 | } 280 | }); 281 | }); 282 | 283 | it('should add originalIndent info to block comments', function () { 284 | var ast = rocambole.parse(' /* foo */\n\t\t// bar'); 285 | expect( ast.startToken.next.originalIndent ).to.be(' '); 286 | }); 287 | 288 | it('should not add originalIndent info to line comments', function () { 289 | var ast = rocambole.parse(' /* foo */\n\t\t// bar'); 290 | expect( ast.endToken.originalIndent ).to.be(undefined); 291 | }); 292 | 293 | it('should not add as originalIndent if prev token is not white space', function () { 294 | var ast = rocambole.parse('lorem;/* foo */\n\t\t// bar'); 295 | expect( ast.startToken.next.next.originalIndent ).to.be(undefined); 296 | }); 297 | 298 | it('should not add as originalIndent if prev token is not on a new line', function () { 299 | var ast = rocambole.parse('lorem; /* foo */\n\t\t// bar'); 300 | expect( ast.startToken.next.next.next.originalIndent ).to.be(undefined); 301 | }); 302 | 303 | it('should add as originalIndent if on a new line', function () { 304 | var ast = rocambole.parse('lorem;\n /* foo */\n\t\t// bar'); 305 | expect( ast.startToken.next.next.next.next.originalIndent ).to.be(' '); 306 | }); 307 | }); 308 | 309 | 310 | describe('export BYPASS_RECURSION', function () { 311 | it('should export BYPASS_RECURSION', function () { 312 | expect( rocambole.BYPASS_RECURSION.root ).to.be(true); 313 | }); 314 | }); 315 | 316 | 317 | describe('empty program', function () { 318 | it('should not throw if program is empty', function () { 319 | expect(function(){ 320 | rocambole.parse(''); 321 | }).not.throwError(); 322 | }); 323 | it('should return augmented AST', function () { 324 | var ast = rocambole.parse(''); 325 | expect(ast).to.eql({ 326 | type: 'Program', 327 | sourceType: 'script', 328 | body: [], 329 | range: [0,0], 330 | comments: [], 331 | tokens: [], 332 | // we check toString behavior later 333 | toString: ast.toString, 334 | startToken: null, 335 | endToken: null, 336 | depth: 0 337 | }); 338 | }); 339 | it('toString should return proper value', function() { 340 | var ast = rocambole.parse(''); 341 | expect(ast.toString()).to.be(''); 342 | }); 343 | }); 344 | 345 | 346 | describe('support anything that implements `toString` as input', function () { 347 | it('should support arrays', function () { 348 | var ast = rocambole.parse([1,2,3]); 349 | expect(ast.body[0].toString()).to.eql('1,2,3'); 350 | }); 351 | it('should support functions', function () { 352 | var ast = rocambole.parse(function doStuff(){ 353 | doStuff(1, 2); 354 | }); 355 | expect(ast.body[0].type).to.be('FunctionDeclaration'); 356 | }); 357 | }); 358 | 359 | 360 | describe('sparse array', function() { 361 | // yes, people shold not be writting code like this, but we should not 362 | // bail when that happens 363 | it('should not fail on sparse arrays', function() { 364 | var ast = rocambole.parse('[,3,[,4]]'); 365 | expect(ast.toString()).to.eql('[,3,[,4]]'); 366 | var elements = ast.body[0].expression.elements; 367 | expect(elements[0]).to.be(null); 368 | expect(elements[1].type).to.be('Literal'); 369 | expect(elements[1].value).to.be(3); 370 | }); 371 | }); 372 | 373 | describe('custom parseFn', function() { 374 | var _parseFn; 375 | var _parseContext; 376 | var _parseOptions; 377 | var empty = { 378 | type: 'Program', 379 | body: [], 380 | range: [0,0], 381 | comments: [], 382 | tokens: [] 383 | }; 384 | 385 | beforeEach(function() { 386 | _parseFn = rocambole.parseFn; 387 | _parseContext = rocambole.parseContext; 388 | _parseOptions = rocambole.parseOptions; 389 | }); 390 | 391 | afterEach(function() { 392 | rocambole.parseFn = _parseFn; 393 | rocambole.parseContext = _parseContext; 394 | rocambole.parseOptions = _parseOptions; 395 | rocambole.parseOptions.tokens = true; 396 | }); 397 | 398 | it('should allow global override of parseFn', function() { 399 | var obj = {}; 400 | 401 | expect(rocambole.parseOptions).to.eql({ 402 | range: true, 403 | comment: true, 404 | tokens: true 405 | }); 406 | 407 | rocambole.parseOptions.tokens = 567; 408 | 409 | rocambole.parseContext = obj; 410 | rocambole.parseFn = function(source, opts) { 411 | expect(this).to.be(obj); 412 | expect(opts).to.eql({ 413 | loc: true, 414 | foo: 'bar', 415 | range: 123, 416 | tokens: 567, 417 | comment: true 418 | }); 419 | expect(source).to.eql('bar()'); 420 | return empty; 421 | }; 422 | var result = rocambole.parse('bar()', { 423 | loc: true, 424 | foo: 'bar', 425 | range: 123 426 | }); 427 | expect(result).to.be(empty); 428 | }); 429 | 430 | }); 431 | 432 | }); 433 | 434 | -------------------------------------------------------------------------------- /test/files/crossroads.js: -------------------------------------------------------------------------------- 1 | /** @license 2 | * crossroads 3 | * License: MIT 4 | * Author: Miller Medeiros 5 | * Version: 0.11.0 (2012/10/31 21:44) 6 | */ 7 | 8 | (function (define) { 9 | define(['signals'], function (signals) { 10 | 11 | var crossroads, 12 | _hasOptionalGroupBug, 13 | UNDEF; 14 | 15 | // Helpers ----------- 16 | //==================== 17 | 18 | // IE 7-8 capture optional groups as empty strings while other browsers 19 | // capture as `undefined` 20 | _hasOptionalGroupBug = (/t(.+)?/).exec('t')[1] === ''; 21 | 22 | function arrayIndexOf(arr, val) { 23 | if (arr.indexOf) { 24 | return arr.indexOf(val); 25 | } else { 26 | //Array.indexOf doesn't work on IE 6-7 27 | var n = arr.length; 28 | while (n--) { 29 | if (arr[n] === val) { 30 | return n; 31 | } 32 | } 33 | return -1; 34 | } 35 | } 36 | 37 | function arrayRemove(arr, item) { 38 | var i = arrayIndexOf(arr, item); 39 | if (i !== -1) { 40 | arr.splice(i, 1); 41 | } 42 | } 43 | 44 | function isKind(val, kind) { 45 | return '[object '+ kind +']' === Object.prototype.toString.call(val); 46 | } 47 | 48 | function isRegExp(val) { 49 | return isKind(val, 'RegExp'); 50 | } 51 | 52 | function isArray(val) { 53 | return isKind(val, 'Array'); 54 | } 55 | 56 | function isFunction(val) { 57 | return typeof val === 'function'; 58 | } 59 | 60 | //borrowed from AMD-utils 61 | function typecastValue(val) { 62 | var r; 63 | if (val === null || val === 'null') { 64 | r = null; 65 | } else if (val === 'true') { 66 | r = true; 67 | } else if (val === 'false') { 68 | r = false; 69 | } else if (val === UNDEF || val === 'undefined') { 70 | r = UNDEF; 71 | } else if (val === '' || isNaN(val)) { 72 | //isNaN('') returns false 73 | r = val; 74 | } else { 75 | //parseFloat(null || '') returns NaN 76 | r = parseFloat(val); 77 | } 78 | return r; 79 | } 80 | 81 | function typecastArrayValues(values) { 82 | var n = values.length, 83 | result = []; 84 | while (n--) { 85 | result[n] = typecastValue(values[n]); 86 | } 87 | return result; 88 | } 89 | 90 | //borrowed from AMD-Utils 91 | function decodeQueryString(str, shouldTypecast) { 92 | var queryArr = (str || '').replace('?', '').split('&'), 93 | n = queryArr.length, 94 | obj = {}, 95 | item, val; 96 | while (n--) { 97 | item = queryArr[n].split('='); 98 | val = shouldTypecast ? typecastValue(item[1]) : item[1]; 99 | obj[item[0]] = (typeof val === 'string')? decodeURIComponent(val) : val; 100 | } 101 | return obj; 102 | } 103 | 104 | 105 | // Crossroads -------- 106 | //==================== 107 | 108 | /** 109 | * @constructor 110 | */ 111 | function Crossroads() { 112 | this.bypassed = new signals.Signal(); 113 | this.routed = new signals.Signal(); 114 | this._routes = []; 115 | this._prevRoutes = []; 116 | this._piped = []; 117 | this.resetState(); 118 | } 119 | 120 | Crossroads.prototype = { 121 | 122 | greedy : false, 123 | 124 | greedyEnabled : true, 125 | 126 | ignoreCase : true, 127 | 128 | ignoreState : false, 129 | 130 | shouldTypecast : false, 131 | 132 | normalizeFn : null, 133 | 134 | resetState : function(){ 135 | this._prevRoutes.length = 0; 136 | this._prevMatchedRequest = null; 137 | this._prevBypassedRequest = null; 138 | }, 139 | 140 | create : function () { 141 | return new Crossroads(); 142 | }, 143 | 144 | addRoute : function (pattern, callback, priority) { 145 | var route = new Route(pattern, callback, priority, this); 146 | this._sortedInsert(route); 147 | return route; 148 | }, 149 | 150 | removeRoute : function (route) { 151 | arrayRemove(this._routes, route); 152 | route._destroy(); 153 | }, 154 | 155 | removeAllRoutes : function () { 156 | var n = this.getNumRoutes(); 157 | while (n--) { 158 | this._routes[n]._destroy(); 159 | } 160 | this._routes.length = 0; 161 | }, 162 | 163 | parse : function (request, defaultArgs) { 164 | request = request || ''; 165 | defaultArgs = defaultArgs || []; 166 | 167 | // should only care about different requests if ignoreState isn't true 168 | if ( !this.ignoreState && 169 | (request === this._prevMatchedRequest || 170 | request === this._prevBypassedRequest) ) { 171 | return; 172 | } 173 | 174 | var routes = this._getMatchedRoutes(request), 175 | i = 0, 176 | n = routes.length, 177 | cur; 178 | 179 | if (n) { 180 | this._prevMatchedRequest = request; 181 | 182 | this._notifyPrevRoutes(routes, request); 183 | this._prevRoutes = routes; 184 | //should be incremental loop, execute routes in order 185 | while (i < n) { 186 | cur = routes[i]; 187 | cur.route.matched.dispatch.apply(cur.route.matched, defaultArgs.concat(cur.params)); 188 | cur.isFirst = !i; 189 | this.routed.dispatch.apply(this.routed, defaultArgs.concat([request, cur])); 190 | i += 1; 191 | } 192 | } else { 193 | this._prevBypassedRequest = request; 194 | this.bypassed.dispatch.apply(this.bypassed, defaultArgs.concat([request])); 195 | } 196 | 197 | this._pipeParse(request, defaultArgs); 198 | }, 199 | 200 | _notifyPrevRoutes : function(matchedRoutes, request) { 201 | var i = 0, prev; 202 | while (prev = this._prevRoutes[i++]) { 203 | //check if switched exist since route may be disposed 204 | if(prev.route.switched && this._didSwitch(prev.route, matchedRoutes)) { 205 | prev.route.switched.dispatch(request); 206 | } 207 | } 208 | }, 209 | 210 | _didSwitch : function (route, matchedRoutes){ 211 | var matched, 212 | i = 0; 213 | while (matched = matchedRoutes[i++]) { 214 | // only dispatch switched if it is going to a different route 215 | if (matched.route === route) { 216 | return false; 217 | } 218 | } 219 | return true; 220 | }, 221 | 222 | _pipeParse : function(request, defaultArgs) { 223 | var i = 0, route; 224 | while (route = this._piped[i++]) { 225 | route.parse(request, defaultArgs); 226 | } 227 | }, 228 | 229 | getNumRoutes : function () { 230 | return this._routes.length; 231 | }, 232 | 233 | _sortedInsert : function (route) { 234 | //simplified insertion sort 235 | var routes = this._routes, 236 | n = routes.length; 237 | do { --n; } while (routes[n] && route._priority <= routes[n]._priority); 238 | routes.splice(n+1, 0, route); 239 | }, 240 | 241 | _getMatchedRoutes : function (request) { 242 | var res = [], 243 | routes = this._routes, 244 | n = routes.length, 245 | route; 246 | //should be decrement loop since higher priorities are added at the end of array 247 | while (route = routes[--n]) { 248 | if ((!res.length || this.greedy || route.greedy) && route.match(request)) { 249 | res.push({ 250 | route : route, 251 | params : route._getParamsArray(request) 252 | }); 253 | } 254 | if (!this.greedyEnabled && res.length) { 255 | break; 256 | } 257 | } 258 | return res; 259 | }, 260 | 261 | pipe : function (otherRouter) { 262 | this._piped.push(otherRouter); 263 | }, 264 | 265 | unpipe : function (otherRouter) { 266 | arrayRemove(this._piped, otherRouter); 267 | }, 268 | 269 | toString : function () { 270 | return '[crossroads numRoutes:'+ this.getNumRoutes() +']'; 271 | } 272 | }; 273 | 274 | //"static" instance 275 | crossroads = new Crossroads(); 276 | crossroads.VERSION = '0.11.0'; 277 | 278 | crossroads.NORM_AS_ARRAY = function (req, vals) { 279 | return [vals.vals_]; 280 | }; 281 | 282 | crossroads.NORM_AS_OBJECT = function (req, vals) { 283 | return [vals]; 284 | }; 285 | 286 | 287 | // Route -------------- 288 | //===================== 289 | 290 | /** 291 | * @constructor 292 | */ 293 | function Route(pattern, callback, priority, router) { 294 | var isRegexPattern = isRegExp(pattern), 295 | patternLexer = crossroads.patternLexer; 296 | this._router = router; 297 | this._pattern = pattern; 298 | this._paramsIds = isRegexPattern? null : patternLexer.getParamIds(pattern); 299 | this._optionalParamsIds = isRegexPattern? null : patternLexer.getOptionalParamsIds(pattern); 300 | this._matchRegexp = isRegexPattern? pattern : patternLexer.compilePattern(pattern, router.ignoreCase); 301 | this.matched = new signals.Signal(); 302 | this.switched = new signals.Signal(); 303 | if (callback) { 304 | this.matched.add(callback); 305 | } 306 | this._priority = priority || 0; 307 | } 308 | 309 | Route.prototype = { 310 | 311 | greedy : false, 312 | 313 | rules : void(0), 314 | 315 | match : function (request) { 316 | request = request || ''; 317 | return this._matchRegexp.test(request) && this._validateParams(request); //validate params even if regexp because of `request_` rule. 318 | }, 319 | 320 | _validateParams : function (request) { 321 | var rules = this.rules, 322 | values = this._getParamsObject(request), 323 | key; 324 | for (key in rules) { 325 | // normalize_ isn't a validation rule... (#39) 326 | if(key !== 'normalize_' && rules.hasOwnProperty(key) && ! this._isValidParam(request, key, values)){ 327 | return false; 328 | } 329 | } 330 | return true; 331 | }, 332 | 333 | _isValidParam : function (request, prop, values) { 334 | var validationRule = this.rules[prop], 335 | val = values[prop], 336 | isValid = false, 337 | isQuery = (prop.indexOf('?') === 0); 338 | 339 | if (val == null && this._optionalParamsIds && arrayIndexOf(this._optionalParamsIds, prop) !== -1) { 340 | isValid = true; 341 | } 342 | else if (isRegExp(validationRule)) { 343 | if (isQuery) { 344 | val = values[prop +'_']; //use raw string 345 | } 346 | isValid = validationRule.test(val); 347 | } 348 | else if (isArray(validationRule)) { 349 | if (isQuery) { 350 | val = values[prop +'_']; //use raw string 351 | } 352 | isValid = this._isValidArrayRule(validationRule, val); 353 | } 354 | else if (isFunction(validationRule)) { 355 | isValid = validationRule(val, request, values); 356 | } 357 | 358 | return isValid; //fail silently if validationRule is from an unsupported type 359 | }, 360 | 361 | _isValidArrayRule : function (arr, val) { 362 | if (! this._router.ignoreCase) { 363 | return arrayIndexOf(arr, val) !== -1; 364 | } 365 | 366 | if (typeof val === 'string') { 367 | val = val.toLowerCase(); 368 | } 369 | 370 | var n = arr.length, 371 | item, 372 | compareVal; 373 | 374 | while (n--) { 375 | item = arr[n]; 376 | compareVal = (typeof item === 'string')? item.toLowerCase() : item; 377 | if (compareVal === val) { 378 | return true; 379 | } 380 | } 381 | return false; 382 | }, 383 | 384 | _getParamsObject : function (request) { 385 | var shouldTypecast = this._router.shouldTypecast, 386 | values = crossroads.patternLexer.getParamValues(request, this._matchRegexp, shouldTypecast), 387 | o = {}, 388 | n = values.length, 389 | param, val; 390 | while (n--) { 391 | val = values[n]; 392 | if (this._paramsIds) { 393 | param = this._paramsIds[n]; 394 | if (param.indexOf('?') === 0 && val) { 395 | //make a copy of the original string so array and 396 | //RegExp validation can be applied properly 397 | o[param +'_'] = val; 398 | //update vals_ array as well since it will be used 399 | //during dispatch 400 | val = decodeQueryString(val, shouldTypecast); 401 | values[n] = val; 402 | } 403 | // IE will capture optional groups as empty strings while other 404 | // browsers will capture `undefined` so normalize behavior. 405 | // see: #gh-58, #gh-59, #gh-60 406 | if ( _hasOptionalGroupBug && val === '' && arrayIndexOf(this._optionalParamsIds, param) !== -1 ) { 407 | val = void(0); 408 | values[n] = val; 409 | } 410 | o[param] = val; 411 | } 412 | //alias to paths and for RegExp pattern 413 | o[n] = val; 414 | } 415 | o.request_ = shouldTypecast? typecastValue(request) : request; 416 | o.vals_ = values; 417 | return o; 418 | }, 419 | 420 | _getParamsArray : function (request) { 421 | var norm = this.rules? this.rules.normalize_ : null, 422 | params; 423 | norm = norm || this._router.normalizeFn; // default normalize 424 | if (norm && isFunction(norm)) { 425 | params = norm(request, this._getParamsObject(request)); 426 | } else { 427 | params = this._getParamsObject(request).vals_; 428 | } 429 | return params; 430 | }, 431 | 432 | interpolate : function(replacements) { 433 | var str = crossroads.patternLexer.interpolate(this._pattern, replacements); 434 | if (! this._validateParams(str) ) { 435 | throw new Error('Generated string doesn\'t validate against `Route.rules`.'); 436 | } 437 | return str; 438 | }, 439 | 440 | dispose : function () { 441 | this._router.removeRoute(this); 442 | }, 443 | 444 | _destroy : function () { 445 | this.matched.dispose(); 446 | this.switched.dispose(); 447 | this.matched = this.switched = this._pattern = this._matchRegexp = null; 448 | }, 449 | 450 | toString : function () { 451 | return '[Route pattern:"'+ this._pattern +'", numListeners:'+ this.matched.getNumListeners() +']'; 452 | } 453 | 454 | }; 455 | 456 | 457 | 458 | // Pattern Lexer ------ 459 | //===================== 460 | 461 | crossroads.patternLexer = (function () { 462 | 463 | var 464 | //match chars that should be escaped on string regexp 465 | ESCAPE_CHARS_REGEXP = /[\\.+*?\^$\[\](){}\/'#]/g, 466 | 467 | //trailing slashes (begin/end of string) 468 | LOOSE_SLASHES_REGEXP = /^\/|\/$/g, 469 | LEGACY_SLASHES_REGEXP = /\/$/g, 470 | 471 | //params - everything between `{ }` or `: :` 472 | PARAMS_REGEXP = /(?:\{|:)([^}:]+)(?:\}|:)/g, 473 | 474 | //used to save params during compile (avoid escaping things that 475 | //shouldn't be escaped). 476 | TOKENS = { 477 | 'OS' : { 478 | //optional slashes 479 | //slash between `::` or `}:` or `\w:` or `:{?` or `}{?` or `\w{?` 480 | rgx : /([:}]|\w(?=\/))\/?(:|(?:\{\?))/g, 481 | save : '$1{{id}}$2', 482 | res : '\\/?' 483 | }, 484 | 'RS' : { 485 | //required slashes 486 | //used to insert slash between `:{` and `}{` 487 | rgx : /([:}])\/?(\{)/g, 488 | save : '$1{{id}}$2', 489 | res : '\\/' 490 | }, 491 | 'RQ' : { 492 | //required query string - everything in between `{? }` 493 | rgx : /\{\?([^}]+)\}/g, 494 | //everything from `?` till `#` or end of string 495 | res : '\\?([^#]+)' 496 | }, 497 | 'OQ' : { 498 | //optional query string - everything in between `:? :` 499 | rgx : /:\?([^:]+):/g, 500 | //everything from `?` till `#` or end of string 501 | res : '(?:\\?([^#]*))?' 502 | }, 503 | 'OR' : { 504 | //optional rest - everything in between `: *:` 505 | rgx : /:([^:]+)\*:/g, 506 | res : '(.*)?' // optional group to avoid passing empty string as captured 507 | }, 508 | 'RR' : { 509 | //rest param - everything in between `{ *}` 510 | rgx : /\{([^}]+)\*\}/g, 511 | res : '(.+)' 512 | }, 513 | // required/optional params should come after rest segments 514 | 'RP' : { 515 | //required params - everything between `{ }` 516 | rgx : /\{([^}]+)\}/g, 517 | res : '([^\\/?]+)' 518 | }, 519 | 'OP' : { 520 | //optional params - everything between `: :` 521 | rgx : /:([^:]+):/g, 522 | res : '([^\\/?]+)?\/?' 523 | } 524 | }, 525 | 526 | LOOSE_SLASH = 1, 527 | STRICT_SLASH = 2, 528 | LEGACY_SLASH = 3, 529 | 530 | _slashMode = LOOSE_SLASH; 531 | 532 | 533 | function precompileTokens(){ 534 | var key, cur; 535 | for (key in TOKENS) { 536 | if (TOKENS.hasOwnProperty(key)) { 537 | cur = TOKENS[key]; 538 | cur.id = '__CR_'+ key +'__'; 539 | cur.save = ('save' in cur)? cur.save.replace('{{id}}', cur.id) : cur.id; 540 | cur.rRestore = new RegExp(cur.id, 'g'); 541 | } 542 | } 543 | } 544 | precompileTokens(); 545 | 546 | 547 | function captureVals(regex, pattern) { 548 | var vals = [], match; 549 | // very important to reset lastIndex since RegExp can have "g" flag 550 | // and multiple runs might affect the result, specially if matching 551 | // same string multiple times on IE 7-8 552 | regex.lastIndex = 0; 553 | while (match = regex.exec(pattern)) { 554 | vals.push(match[1]); 555 | } 556 | return vals; 557 | } 558 | 559 | function getParamIds(pattern) { 560 | return captureVals(PARAMS_REGEXP, pattern); 561 | } 562 | 563 | function getOptionalParamsIds(pattern) { 564 | return captureVals(TOKENS.OP.rgx, pattern); 565 | } 566 | 567 | function compilePattern(pattern, ignoreCase) { 568 | pattern = pattern || ''; 569 | 570 | if(pattern){ 571 | if (_slashMode === LOOSE_SLASH) { 572 | pattern = pattern.replace(LOOSE_SLASHES_REGEXP, ''); 573 | } 574 | else if (_slashMode === LEGACY_SLASH) { 575 | pattern = pattern.replace(LEGACY_SLASHES_REGEXP, ''); 576 | } 577 | 578 | //save tokens 579 | pattern = replaceTokens(pattern, 'rgx', 'save'); 580 | //regexp escape 581 | pattern = pattern.replace(ESCAPE_CHARS_REGEXP, '\\$&'); 582 | //restore tokens 583 | pattern = replaceTokens(pattern, 'rRestore', 'res'); 584 | 585 | if (_slashMode === LOOSE_SLASH) { 586 | pattern = '\\/?'+ pattern; 587 | } 588 | } 589 | 590 | if (_slashMode !== STRICT_SLASH) { 591 | //single slash is treated as empty and end slash is optional 592 | pattern += '\\/?'; 593 | } 594 | return new RegExp('^'+ pattern + '$', ignoreCase? 'i' : ''); 595 | } 596 | 597 | function replaceTokens(pattern, regexpName, replaceName) { 598 | var cur, key; 599 | for (key in TOKENS) { 600 | if (TOKENS.hasOwnProperty(key)) { 601 | cur = TOKENS[key]; 602 | pattern = pattern.replace(cur[regexpName], cur[replaceName]); 603 | } 604 | } 605 | return pattern; 606 | } 607 | 608 | function getParamValues(request, regexp, shouldTypecast) { 609 | var vals = regexp.exec(request); 610 | if (vals) { 611 | vals.shift(); 612 | if (shouldTypecast) { 613 | vals = typecastArrayValues(vals); 614 | } 615 | } 616 | return vals; 617 | } 618 | 619 | function interpolate(pattern, replacements) { 620 | if (typeof pattern !== 'string') { 621 | throw new Error('Route pattern should be a string.'); 622 | } 623 | 624 | var replaceFn = function(match, prop){ 625 | var val; 626 | if (prop in replacements) { 627 | // make sure value is a string see #gh-54 628 | val = String(replacements[prop]); 629 | if (match.indexOf('*') === -1 && val.indexOf('/') !== -1) { 630 | throw new Error('Invalid value "'+ val +'" for segment "'+ match +'".'); 631 | } 632 | } 633 | else if (match.indexOf('{') !== -1) { 634 | throw new Error('The segment '+ match +' is required.'); 635 | } 636 | else { 637 | val = ''; 638 | } 639 | return val; 640 | }; 641 | 642 | if (! TOKENS.OS.trail) { 643 | TOKENS.OS.trail = new RegExp('(?:'+ TOKENS.OS.id +')+$'); 644 | } 645 | 646 | return pattern 647 | .replace(TOKENS.OS.rgx, TOKENS.OS.save) 648 | .replace(PARAMS_REGEXP, replaceFn) 649 | .replace(TOKENS.OS.trail, '') // remove trailing 650 | .replace(TOKENS.OS.rRestore, '/'); // add slash between segments 651 | } 652 | 653 | //API 654 | return { 655 | strict : function(){ 656 | _slashMode = STRICT_SLASH; 657 | }, 658 | loose : function(){ 659 | _slashMode = LOOSE_SLASH; 660 | }, 661 | legacy : function(){ 662 | _slashMode = LEGACY_SLASH; 663 | }, 664 | getParamIds : getParamIds, 665 | getOptionalParamsIds : getOptionalParamsIds, 666 | getParamValues : getParamValues, 667 | compilePattern : compilePattern, 668 | interpolate : interpolate 669 | }; 670 | 671 | }()); 672 | 673 | 674 | return crossroads; 675 | }); 676 | }(typeof define === 'function' && define.amd ? define : function (deps, factory) { 677 | if (typeof module !== 'undefined' && module.exports) { //Node 678 | module.exports = factory(require(deps[0])); 679 | } else { 680 | /*jshint sub:true */ 681 | window['crossroads'] = factory(window[deps[0]]); 682 | } 683 | })); 684 | --------------------------------------------------------------------------------