├── .arcconfig ├── .arclint ├── .gitignore ├── .travis.yml ├── README.md ├── bower.json ├── browser-test └── structuredjs_test.html ├── demo ├── demo.css └── demo.js ├── external ├── ace-mode-javascript.js ├── ace-orig.js ├── ace-worker-javascript.js ├── esprima.js ├── jquery.min.js ├── qunit.css ├── qunit.js ├── rainbow │ ├── language │ │ ├── generic.js │ │ └── javascript.js │ └── rainbow.min.js └── underscore.min.js ├── index.html ├── package.json ├── pretty-display ├── display-demo.css ├── display.css ├── display.js ├── index.html └── textmate-lite.css ├── structured.js ├── testrunner.js └── tests.js /.arcconfig: -------------------------------------------------------------------------------- 1 | { 2 | "conduit_uri": "https://phabricator.khanacademy.org/", 3 | "lint.engine": "ArcanistConfigurationDrivenLintEngine" 4 | } 5 | -------------------------------------------------------------------------------- /.arclint: -------------------------------------------------------------------------------- 1 | { 2 | "linters": { 3 | "khan-linter": { 4 | "type": "script-and-regex", 5 | "script-and-regex.script": "ka-lint --always-exit-0 --blacklist=yes --propose-arc-fixes", 6 | "script-and-regex.regex": "\/^((?P[^:]*):(?P\\d+):((?P\\d+):)? (?P((?PE)|(?PW))\\S+) (?P[^\\x00]*)(\\x00(?P[^\\x00]*)\\x00(?P[^\\x00]*)\\x00)?)|(?PSKIPPING.*)$\/m" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.12" 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **structured.js** is a Javascript library that provides a simple interface for verifying the structure of Javascript code, backed by the abstract syntax tree generated by Esprima. It is particularly useful in checking beginner code to provide feedback as part of [Khan Academy's CS curriculum](https://www.khanacademy.org/cs). 2 | 3 | Structured.js works in-browser ``, or as a standalone npm module. 4 | 5 | A structure is any valid Javascript code which contains blanks ( _ characters) and stand-ins ($str) to match values. The structure is sensitive to nesting and ordering. The matcher only requires that code contain the structure -- extra code has no effect. 6 | 7 | ### Demo 8 | 9 | **[Try structured.js yourself](http://khan.github.io/structuredjs/index.html)** to see it in action. 10 | 11 | Also check out the [pretty display demo](http://khan.github.io/structuredjs/pretty-display/index.html) for more user-friendly structures. 12 | 13 | ### Examples 14 | 15 | var structure = function() { 16 | if (_) { 17 | _ += _; 18 | for (var $a = _; $a < $b; $a += _) { 19 | _($a, $b, 30, 30); 20 | } 21 | } 22 | }; 23 | 24 | var code = "/* some code */"; 25 | 26 | var result = Structured.match(code, structure); 27 | 28 | Returns true for the code: 29 | 30 | if (y > 30 && x > 13) { 31 | x += y; 32 | for (var i = 0; i < 100; i += 1) { 33 | rect(i, 100, 30, 30); 34 | bar(); 35 | } 36 | } 37 | 38 | **[Check out the demo](http://khan.github.io/structuredjs/index.html)** for more, or look at the tests. 39 | 40 | ### Advanced -- Variable Callbacks 41 | 42 | To allow tighter control over what exactly is allowed to match your $variable, you may provide a mapping from variable names to function callbacks. These callbacks can enable NOT, OR, and AND functionality on the wildcard variables, for example. 43 | 44 | Callback parameters should be the same as the name of the wildcard variables they are matching. The callback takes in a proposed value for the variable and accepts/rejects it by returning a boolean. The callback may instead return an object such as `{failure: "failure message"}` as well if you'd like to explain exactly why this value is not allowed. 45 | 46 | For instance, say we want to check the value we assign to a var -- check that it is really big, and that it is bigger than whatever we increment it by. It would look like this: 47 | 48 | var structure = function() {var _ = $num; $num += $incr; }; 49 | var code = "var foo = 400; foo += 3;"; 50 | var varCallbacks = [ 51 | function($num) { 52 | return num.value > 100; // Just return true/false 53 | }, 54 | function($num, $incr) { 55 | if (num.value <= incr.value) { 56 | // Return the failure message 57 | return {failure: "The increment must be smaller than the number."}; 58 | } 59 | return true; 60 | } 61 | ]; 62 | var match = Structured.match(code, structure, {varCallbacks: varCallbacks}); 63 | if (!match) { 64 | // varCallbacks.failure contains the error message, if any. 65 | console.log("The problem is: " + varCallbacks.failure); 66 | } 67 | 68 | Note that the callbacks receive objects that contain a subtree of the [Esprima](http://esprima.org) parse tree, not a raw value. Also note that the callbacks run statically, not dynamically -- so, you will only be able to directly check literal values (i.e., 48), not computed values (24*2, myVar, etc). The callbacks also ignore any variable callbacks for variables that do not actually appear in the structure you've passed in. 69 | 70 | Go to [the demo](http://khan.github.io/structuredjs/index.html) to try it out. 71 | 72 | ### Tests 73 | 74 | Run structured.js tests with `npm test` or by opening browser-test/index.html. 75 | 76 | ### Dependencies 77 | 78 | [Esprima](http://esprima.org) and [UnderscoreJS](http://underscorejs.org) for the framework, 79 | [QUnit](http://qunitjs.com/) for the test suite, 80 | [RainbowJS](http://craig.is/making/rainbows/) for prettified structures. 81 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "structured", 3 | "version": "0.1.3", 4 | "homepage": "https://github.com/Khan/structuredjs", 5 | "authors": [ 6 | "John Resig " 7 | ], 8 | "description": "Simple interface for checking structure of JS code against a template, backed by Esprima.", 9 | "main": "structured.js", 10 | "moduleType": [ 11 | "globals" 12 | ], 13 | "keywords": [ 14 | "parsing", 15 | "analysis", 16 | "ast", 17 | "checker", 18 | "structure" 19 | ], 20 | "license": "BSD", 21 | "ignore": [ 22 | "**/.*", 23 | "node_modules", 24 | "bower_components", 25 | "test", 26 | "tests" 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /browser-test/structuredjs_test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | StructuredJS unit tests 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 |
16 | 17 | -------------------------------------------------------------------------------- /demo/demo.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: white; 3 | font-family: Helvetica; 4 | margin: 30px 0px; 5 | padding:0px; 6 | text-align:center; 7 | } 8 | #container { 9 | width:1000px; 10 | margin:0px auto; 11 | text-align:left; 12 | } 13 | 14 | .code-editor { 15 | display: inline-block; 16 | height: 300px; 17 | width: 450px; 18 | border: 1px solid gray; 19 | margin-top: .6em; 20 | margin-left: 1em; 21 | margin-bottom: 1.6em; 22 | } 23 | #structure { 24 | margin-right: 2em; 25 | } 26 | #results { 27 | display: inline-block; 28 | text-transform: lowercase; 29 | color: #f29240; 30 | font-weight: 200px; 31 | letter-spacing: 1px; 32 | margin-top: .3em; 33 | padding: 20px; 34 | font-size: 1.4em 35 | } 36 | #run-button { 37 | font-size: 20px; 38 | border: 0px; 39 | height: 44px; 40 | margin-left: 1em; 41 | background-color: #f29240; 42 | width: 200px; 43 | cursor: pointer; 44 | color: #555; 45 | letter-spacing: 1px; 46 | } 47 | #run-button:hover { 48 | background-color: sandybrown; 49 | } 50 | #run-button:active { 51 | position:relative; 52 | top:1px; 53 | } 54 | #result-wrapper { 55 | display: inline-block; 56 | width: 400px; 57 | vertical-align: top; 58 | } 59 | .match-fail-message { 60 | margin: 1em; 61 | margin-left: 3em; 62 | color: #666; 63 | } 64 | 65 | #remaining { 66 | 67 | } 68 | 69 | #var-callbacks { 70 | visibility: hidden; 71 | } 72 | .var-callbacks-show-hide { 73 | margin-left: 2em; 74 | cursor: pointer; 75 | padding: 2px 6px; 76 | background-color: #DDD; 77 | } 78 | .gen-test { 79 | border-bottom: 1px dotted gray; 80 | } 81 | .gen-test:hover { 82 | cursor: pointer; 83 | border-bottom: 1px solid black; 84 | } 85 | .test-wrapper { 86 | margin-top: 10px; 87 | border-top: 2px dashed lightslategray; 88 | padding-top: 10px; 89 | padding-left: 200px; 90 | margin-bottom: 50px; 91 | } 92 | .test-code { 93 | background-color: #CECECE; 94 | padding: 10px; 95 | font-family: monospace; 96 | width: 600px; 97 | } 98 | .editor-labels { 99 | text-transform: lowercase; 100 | font-size: .9em; 101 | font-weight: 400; 102 | color: #666; 103 | letter-spacing: 1px; 104 | } 105 | .structure-label { 106 | margin-left: 30px; 107 | 108 | } 109 | .code-label { 110 | margin-left: 660px; 111 | } 112 | .page-label { 113 | text-align: center; 114 | font-family: helvetica; 115 | letter-spacing: .2em; 116 | text-transform: lowercase; 117 | font-variant: small-caps; 118 | color: rgb(223, 252, 255); 119 | font-size: 2.4em; 120 | padding-bottom:.2em; 121 | margin-bottom: .7em; 122 | color: teal; 123 | border-bottom: 1px solid lightslategray; 124 | } 125 | footer { 126 | font-size: .8em; 127 | margin-top: 50px; 128 | color: #999; 129 | text-align: right; 130 | padding-right: 100px; 131 | } 132 | 133 | a, a:visited { 134 | color: #22A; 135 | text-decoration: none; 136 | } 137 | a:hover { 138 | color: #AAA; 139 | } -------------------------------------------------------------------------------- /demo/demo.js: -------------------------------------------------------------------------------- 1 | /* Simple control code for allowing user to type code into two editors, 2 | and check whether the code in the first editor matches the wildcard 3 | structure in the second editor. 4 | 5 | Also generates QUnit test code based on the editor contents. */ 6 | 7 | $(document).ready(function() { 8 | // Set up the editors (disable workers so we can run locally as well.) 9 | var editorStructure = ace.edit("structure"); 10 | setupEditor(editorStructure); 11 | var editorTest = ace.edit("test-code"); 12 | setupEditor(editorTest); 13 | var editorCallbacks = ace.edit("var-callbacks"); 14 | setupEditor(editorCallbacks); 15 | var oldFocus = editorStructure; 16 | 17 | // Run Structured.match when the user presses the button. 18 | $("#run-button").click(function(evt) { 19 | var structure = "function() {\n" + editorStructure.getValue() + "\n}"; 20 | var code = editorTest.getValue(); 21 | // Pull in the object with function callbacks. 22 | eval("var varCallbacks = " + editorCallbacks.getValue()); 23 | var message, errorMessage; 24 | try { 25 | var result = !!Structured.match(code, structure, 26 | {varCallbacks: varCallbacks}); 27 | message = "Match: " + result; 28 | errorMessage = varCallbacks.failure || ""; 29 | } catch (error) { 30 | message = ""; 31 | errorMessage = error; 32 | } 33 | $("#results").hide().html(message).fadeIn(); 34 | $(".match-fail-message").html(errorMessage); 35 | $(".test-wrapper").hide(); 36 | oldFocus.focus(); 37 | makeTest(structure, code, editorCallbacks.getValue(), result); 38 | }); 39 | 40 | // Show QUnit test code 41 | $(".gen-test").click(function(evt) { 42 | $("#run-button").click(); 43 | $(".test-wrapper").show(); 44 | }); 45 | 46 | 47 | $(".var-callbacks-show-hide").click(function(evt) { 48 | $("#var-callbacks").css('visibility', function(i, visibility) { 49 | return visibility === "visible" ? "hidden" : "visible"; 50 | }); 51 | }); 52 | 53 | // Output results on the initial load. 54 | $("#run-button").click(); 55 | 56 | 57 | function setupEditor(editor) { 58 | editor.getSession().setUseWorker(false); 59 | editor.getSession().setMode("ace/mode/javascript"); 60 | editor.renderer.setShowGutter(false); 61 | editor.renderer.setPadding(6); 62 | // Save the user's focus so we can restore it afterwards. 63 | editor.on("focus", function() { 64 | oldFocus = editor; 65 | }); 66 | } 67 | }); 68 | 69 | /* Generates the QUnit test code based on editor contents. 70 | Handles multiline string nonsense. */ 71 | function makeTest(structure, code, editorCallbacks, result) { 72 | var testCode = "// QUnit test code \n"; 73 | testCode += "editorCallbacks = " + editorCallbacks + ";"; 74 | testCode += "\nstructure = " + structure + ";"; 75 | testCode += "\ncode = \" \\n \\ \n"; 76 | _.each(code.split("\n"), function(line) { 77 | testCode += line + " \\n \\ \n"; 78 | }); 79 | testCode += "\"; \n"; 80 | testCode += "equal(Structured.match(code, structure, " + 81 | "{editorCallbacks: editorCallbacks}),\n\t" + 82 | result + ", \"message\");"; 83 | $(".test-code").hide().html(testCode).fadeIn(); 84 | } 85 | -------------------------------------------------------------------------------- /external/ace-mode-javascript.js: -------------------------------------------------------------------------------- 1 | /* ***** BEGIN LICENSE BLOCK ***** 2 | * Distributed under the BSD license: 3 | * 4 | * Copyright (c) 2010, Ajax.org B.V. 5 | * All rights reserved. 6 | * 7 | * Redistribution and use in source and binary forms, with or without 8 | * modification, are permitted provided that the following conditions are met: 9 | * * Redistributions of source code must retain the above copyright 10 | * notice, this list of conditions and the following disclaimer. 11 | * * Redistributions in binary form must reproduce the above copyright 12 | * notice, this list of conditions and the following disclaimer in the 13 | * documentation and/or other materials provided with the distribution. 14 | * * Neither the name of Ajax.org B.V. nor the 15 | * names of its contributors may be used to endorse or promote products 16 | * derived from this software without specific prior written permission. 17 | * 18 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | * DISCLAIMED. IN NO EVENT SHALL AJAX.ORG B.V. BE LIABLE FOR ANY 22 | * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 25 | * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | * 29 | * ***** END LICENSE BLOCK ***** */ 30 | 31 | ace.define('ace/mode/javascript', ['require', 'exports', 'module' , 'ace/lib/oop', 'ace/mode/text', 'ace/tokenizer', 'ace/mode/javascript_highlight_rules', 'ace/mode/matching_brace_outdent', 'ace/range', 'ace/worker/worker_client', 'ace/mode/behaviour/cstyle', 'ace/mode/folding/cstyle'], function(require, exports, module) { 32 | 33 | 34 | var oop = require("../lib/oop"); 35 | var TextMode = require("./text").Mode; 36 | var Tokenizer = require("../tokenizer").Tokenizer; 37 | var JavaScriptHighlightRules = require("./javascript_highlight_rules").JavaScriptHighlightRules; 38 | var MatchingBraceOutdent = require("./matching_brace_outdent").MatchingBraceOutdent; 39 | var Range = require("../range").Range; 40 | var WorkerClient = require("../worker/worker_client").WorkerClient; 41 | var CstyleBehaviour = require("./behaviour/cstyle").CstyleBehaviour; 42 | var CStyleFoldMode = require("./folding/cstyle").FoldMode; 43 | 44 | var Mode = function() { 45 | this.$tokenizer = new Tokenizer(new JavaScriptHighlightRules().getRules()); 46 | this.$outdent = new MatchingBraceOutdent(); 47 | this.$behaviour = new CstyleBehaviour(); 48 | this.foldingRules = new CStyleFoldMode(); 49 | }; 50 | oop.inherits(Mode, TextMode); 51 | 52 | (function() { 53 | 54 | this.lineCommentStart = "//"; 55 | this.blockComment = {start: "/*", end: "*/"}; 56 | 57 | this.getNextLineIndent = function(state, line, tab) { 58 | var indent = this.$getIndent(line); 59 | 60 | var tokenizedLine = this.$tokenizer.getLineTokens(line, state); 61 | var tokens = tokenizedLine.tokens; 62 | var endState = tokenizedLine.state; 63 | 64 | if (tokens.length && tokens[tokens.length-1].type == "comment") { 65 | return indent; 66 | } 67 | 68 | if (state == "start" || state == "no_regex") { 69 | var match = line.match(/^.*(?:\bcase\b.*\:|[\{\(\[])\s*$/); 70 | if (match) { 71 | indent += tab; 72 | } 73 | } else if (state == "doc-start") { 74 | if (endState == "start" || endState == "no_regex") { 75 | return ""; 76 | } 77 | var match = line.match(/^\s*(\/?)\*/); 78 | if (match) { 79 | if (match[1]) { 80 | indent += " "; 81 | } 82 | indent += "* "; 83 | } 84 | } 85 | 86 | return indent; 87 | }; 88 | 89 | this.checkOutdent = function(state, line, input) { 90 | return this.$outdent.checkOutdent(line, input); 91 | }; 92 | 93 | this.autoOutdent = function(state, doc, row) { 94 | this.$outdent.autoOutdent(doc, row); 95 | }; 96 | 97 | this.createWorker = function(session) { 98 | var worker = new WorkerClient(["ace"], "ace/mode/javascript_worker", "JavaScriptWorker"); 99 | worker.attachToDocument(session.getDocument()); 100 | 101 | worker.on("jslint", function(results) { 102 | session.setAnnotations(results.data); 103 | }); 104 | 105 | worker.on("terminate", function() { 106 | session.clearAnnotations(); 107 | }); 108 | 109 | return worker; 110 | }; 111 | 112 | }).call(Mode.prototype); 113 | 114 | exports.Mode = Mode; 115 | }); 116 | 117 | ace.define('ace/mode/javascript_highlight_rules', ['require', 'exports', 'module' , 'ace/lib/oop', 'ace/mode/doc_comment_highlight_rules', 'ace/mode/text_highlight_rules'], function(require, exports, module) { 118 | 119 | 120 | var oop = require("../lib/oop"); 121 | var DocCommentHighlightRules = require("./doc_comment_highlight_rules").DocCommentHighlightRules; 122 | var TextHighlightRules = require("./text_highlight_rules").TextHighlightRules; 123 | 124 | var JavaScriptHighlightRules = function() { 125 | var keywordMapper = this.createKeywordMapper({ 126 | "variable.language": 127 | "Array|Boolean|Date|Function|Iterator|Number|Object|RegExp|String|Proxy|" + // Constructors 128 | "Namespace|QName|XML|XMLList|" + // E4X 129 | "ArrayBuffer|Float32Array|Float64Array|Int16Array|Int32Array|Int8Array|" + 130 | "Uint16Array|Uint32Array|Uint8Array|Uint8ClampedArray|" + 131 | "Error|EvalError|InternalError|RangeError|ReferenceError|StopIteration|" + // Errors 132 | "SyntaxError|TypeError|URIError|" + 133 | "decodeURI|decodeURIComponent|encodeURI|encodeURIComponent|eval|isFinite|" + // Non-constructor functions 134 | "isNaN|parseFloat|parseInt|" + 135 | "JSON|Math|" + // Other 136 | "this|arguments|prototype|window|document" , // Pseudo 137 | "keyword": 138 | "const|yield|import|get|set|" + 139 | "break|case|catch|continue|default|delete|do|else|finally|for|function|" + 140 | "if|in|instanceof|new|return|switch|throw|try|typeof|let|var|while|with|debugger|" + 141 | "__parent__|__count__|escape|unescape|with|__proto__|" + 142 | "class|enum|extends|super|export|implements|private|public|interface|package|protected|static", 143 | "storage.type": 144 | "const|let|var|function", 145 | "constant.language": 146 | "null|Infinity|NaN|undefined", 147 | "support.function": 148 | "alert", 149 | "constant.language.boolean": "true|false" 150 | }, "identifier"); 151 | var kwBeforeRe = "case|do|else|finally|in|instanceof|return|throw|try|typeof|yield|void"; 152 | var identifierRe = "[a-zA-Z\\$_\u00a1-\uffff][a-zA-Z\\d\\$_\u00a1-\uffff]*\\b"; 153 | 154 | var escapedRe = "\\\\(?:x[0-9a-fA-F]{2}|" + // hex 155 | "u[0-9a-fA-F]{4}|" + // unicode 156 | "[0-2][0-7]{0,2}|" + // oct 157 | "3[0-6][0-7]?|" + // oct 158 | "37[0-7]?|" + // oct 159 | "[4-7][0-7]?|" + //oct 160 | ".)"; 161 | 162 | this.$rules = { 163 | "no_regex" : [ 164 | { 165 | token : "comment", 166 | regex : /\/\/.*$/ 167 | }, 168 | DocCommentHighlightRules.getStartRule("doc-start"), 169 | { 170 | token : "comment", // multi line comment 171 | regex : /\/\*/, 172 | next : "comment" 173 | }, { 174 | token : "string", 175 | regex : "'(?=.)", 176 | next : "qstring" 177 | }, { 178 | token : "string", 179 | regex : '"(?=.)', 180 | next : "qqstring" 181 | }, { 182 | token : "constant.numeric", // hex 183 | regex : /0[xX][0-9a-fA-F]+\b/ 184 | }, { 185 | token : "constant.numeric", // float 186 | regex : /[+-]?\d+(?:(?:\.\d*)?(?:[eE][+-]?\d+)?)?\b/ 187 | }, { 188 | token : [ 189 | "storage.type", "punctuation.operator", "support.function", 190 | "punctuation.operator", "entity.name.function", "text","keyword.operator" 191 | ], 192 | regex : "(" + identifierRe + ")(\\.)(prototype)(\\.)(" + identifierRe +")(\\s*)(=)", 193 | next: "function_arguments" 194 | }, { 195 | token : [ 196 | "storage.type", "punctuation.operator", "entity.name.function", "text", 197 | "keyword.operator", "text", "storage.type", "text", "paren.lparen" 198 | ], 199 | regex : "(" + identifierRe + ")(\\.)(" + identifierRe +")(\\s*)(=)(\\s*)(function)(\\s*)(\\()", 200 | next: "function_arguments" 201 | }, { 202 | token : [ 203 | "entity.name.function", "text", "keyword.operator", "text", "storage.type", 204 | "text", "paren.lparen" 205 | ], 206 | regex : "(" + identifierRe +")(\\s*)(=)(\\s*)(function)(\\s*)(\\()", 207 | next: "function_arguments" 208 | }, { 209 | token : [ 210 | "storage.type", "punctuation.operator", "entity.name.function", "text", 211 | "keyword.operator", "text", 212 | "storage.type", "text", "entity.name.function", "text", "paren.lparen" 213 | ], 214 | regex : "(" + identifierRe + ")(\\.)(" + identifierRe +")(\\s*)(=)(\\s*)(function)(\\s+)(\\w+)(\\s*)(\\()", 215 | next: "function_arguments" 216 | }, { 217 | token : [ 218 | "storage.type", "text", "entity.name.function", "text", "paren.lparen" 219 | ], 220 | regex : "(function)(\\s+)(" + identifierRe + ")(\\s*)(\\()", 221 | next: "function_arguments" 222 | }, { 223 | token : [ 224 | "entity.name.function", "text", "punctuation.operator", 225 | "text", "storage.type", "text", "paren.lparen" 226 | ], 227 | regex : "(" + identifierRe + ")(\\s*)(:)(\\s*)(function)(\\s*)(\\()", 228 | next: "function_arguments" 229 | }, { 230 | token : [ 231 | "text", "text", "storage.type", "text", "paren.lparen" 232 | ], 233 | regex : "(:)(\\s*)(function)(\\s*)(\\()", 234 | next: "function_arguments" 235 | }, { 236 | token : "keyword", 237 | regex : "(?:" + kwBeforeRe + ")\\b", 238 | next : "start" 239 | }, { 240 | token : ["punctuation.operator", "support.function"], 241 | regex : /(\.)(s(?:h(?:ift|ow(?:Mod(?:elessDialog|alDialog)|Help))|croll(?:X|By(?:Pages|Lines)?|Y|To)?|t(?:opzzzz|rike)|i(?:n|zeToContent|debar|gnText)|ort|u(?:p|b(?:str(?:ing)?)?)|pli(?:ce|t)|e(?:nd|t(?:Re(?:sizable|questHeader)|M(?:i(?:nutes|lliseconds)|onth)|Seconds|Ho(?:tKeys|urs)|Year|Cursor|Time(?:out)?|Interval|ZOptions|Date|UTC(?:M(?:i(?:nutes|lliseconds)|onth)|Seconds|Hours|Date|FullYear)|FullYear|Active)|arch)|qrt|lice|avePreferences|mall)|h(?:ome|andleEvent)|navigate|c(?:har(?:CodeAt|At)|o(?:s|n(?:cat|textual|firm)|mpile)|eil|lear(?:Timeout|Interval)?|a(?:ptureEvents|ll)|reate(?:StyleSheet|Popup|EventObject))|t(?:o(?:GMTString|S(?:tring|ource)|U(?:TCString|pperCase)|Lo(?:caleString|werCase))|est|a(?:n|int(?:Enabled)?))|i(?:s(?:NaN|Finite)|ndexOf|talics)|d(?:isableExternalCapture|ump|etachEvent)|u(?:n(?:shift|taint|escape|watch)|pdateCommands)|j(?:oin|avaEnabled)|p(?:o(?:p|w)|ush|lugins.refresh|a(?:ddings|rse(?:Int|Float)?)|r(?:int|ompt|eference))|e(?:scape|nableExternalCapture|val|lementFromPoint|x(?:p|ec(?:Script|Command)?))|valueOf|UTC|queryCommand(?:State|Indeterm|Enabled|Value)|f(?:i(?:nd|le(?:ModifiedDate|Size|CreatedDate|UpdatedDate)|xed)|o(?:nt(?:size|color)|rward)|loor|romCharCode)|watch|l(?:ink|o(?:ad|g)|astIndexOf)|a(?:sin|nchor|cos|t(?:tachEvent|ob|an(?:2)?)|pply|lert|b(?:s|ort))|r(?:ou(?:nd|teEvents)|e(?:size(?:By|To)|calc|turnValue|place|verse|l(?:oad|ease(?:Capture|Events)))|andom)|g(?:o|et(?:ResponseHeader|M(?:i(?:nutes|lliseconds)|onth)|Se(?:conds|lection)|Hours|Year|Time(?:zoneOffset)?|Da(?:y|te)|UTC(?:M(?:i(?:nutes|lliseconds)|onth)|Seconds|Hours|Da(?:y|te)|FullYear)|FullYear|A(?:ttention|llResponseHeaders)))|m(?:in|ove(?:B(?:y|elow)|To(?:Absolute)?|Above)|ergeAttributes|a(?:tch|rgins|x))|b(?:toa|ig|o(?:ld|rderWidths)|link|ack))\b(?=\()/ 242 | }, { 243 | token : ["punctuation.operator", "support.function.dom"], 244 | regex : /(\.)(s(?:ub(?:stringData|mit)|plitText|e(?:t(?:NamedItem|Attribute(?:Node)?)|lect))|has(?:ChildNodes|Feature)|namedItem|c(?:l(?:ick|o(?:se|neNode))|reate(?:C(?:omment|DATASection|aption)|T(?:Head|extNode|Foot)|DocumentFragment|ProcessingInstruction|E(?:ntityReference|lement)|Attribute))|tabIndex|i(?:nsert(?:Row|Before|Cell|Data)|tem)|open|delete(?:Row|C(?:ell|aption)|T(?:Head|Foot)|Data)|focus|write(?:ln)?|a(?:dd|ppend(?:Child|Data))|re(?:set|place(?:Child|Data)|move(?:NamedItem|Child|Attribute(?:Node)?)?)|get(?:NamedItem|Element(?:sBy(?:Name|TagName)|ById)|Attribute(?:Node)?)|blur)\b(?=\()/ 245 | }, { 246 | token : ["punctuation.operator", "support.constant"], 247 | regex : /(\.)(s(?:ystemLanguage|cr(?:ipts|ollbars|een(?:X|Y|Top|Left))|t(?:yle(?:Sheets)?|atus(?:Text|bar)?)|ibling(?:Below|Above)|ource|uffixes|e(?:curity(?:Policy)?|l(?:ection|f)))|h(?:istory|ost(?:name)?|as(?:h|Focus))|y|X(?:MLDocument|SLDocument)|n(?:ext|ame(?:space(?:s|URI)|Prop))|M(?:IN_VALUE|AX_VALUE)|c(?:haracterSet|o(?:n(?:structor|trollers)|okieEnabled|lorDepth|mp(?:onents|lete))|urrent|puClass|l(?:i(?:p(?:boardData)?|entInformation)|osed|asses)|alle(?:e|r)|rypto)|t(?:o(?:olbar|p)|ext(?:Transform|Indent|Decoration|Align)|ags)|SQRT(?:1_2|2)|i(?:n(?:ner(?:Height|Width)|put)|ds|gnoreCase)|zIndex|o(?:scpu|n(?:readystatechange|Line)|uter(?:Height|Width)|p(?:sProfile|ener)|ffscreenBuffering)|NEGATIVE_INFINITY|d(?:i(?:splay|alog(?:Height|Top|Width|Left|Arguments)|rectories)|e(?:scription|fault(?:Status|Ch(?:ecked|arset)|View)))|u(?:ser(?:Profile|Language|Agent)|n(?:iqueID|defined)|pdateInterval)|_content|p(?:ixelDepth|ort|ersonalbar|kcs11|l(?:ugins|atform)|a(?:thname|dding(?:Right|Bottom|Top|Left)|rent(?:Window|Layer)?|ge(?:X(?:Offset)?|Y(?:Offset)?))|r(?:o(?:to(?:col|type)|duct(?:Sub)?|mpter)|e(?:vious|fix)))|e(?:n(?:coding|abledPlugin)|x(?:ternal|pando)|mbeds)|v(?:isibility|endor(?:Sub)?|Linkcolor)|URLUnencoded|P(?:I|OSITIVE_INFINITY)|f(?:ilename|o(?:nt(?:Size|Family|Weight)|rmName)|rame(?:s|Element)|gColor)|E|whiteSpace|l(?:i(?:stStyleType|n(?:eHeight|kColor))|o(?:ca(?:tion(?:bar)?|lName)|wsrc)|e(?:ngth|ft(?:Context)?)|a(?:st(?:M(?:odified|atch)|Index|Paren)|yer(?:s|X)|nguage))|a(?:pp(?:MinorVersion|Name|Co(?:deName|re)|Version)|vail(?:Height|Top|Width|Left)|ll|r(?:ity|guments)|Linkcolor|bove)|r(?:ight(?:Context)?|e(?:sponse(?:XML|Text)|adyState))|global|x|m(?:imeTypes|ultiline|enubar|argin(?:Right|Bottom|Top|Left))|L(?:N(?:10|2)|OG(?:10E|2E))|b(?:o(?:ttom|rder(?:Width|RightWidth|BottomWidth|Style|Color|TopWidth|LeftWidth))|ufferDepth|elow|ackground(?:Color|Image)))\b/ 248 | }, { 249 | token : ["storage.type", "punctuation.operator", "support.function.firebug"], 250 | regex : /(console)(\.)(warn|info|log|error|time|timeEnd|assert)\b/ 251 | }, { 252 | token : keywordMapper, 253 | regex : identifierRe 254 | }, { 255 | token : "keyword.operator", 256 | regex : /--|\+\+|[!$%&*+\-~]|===|==|=|!=|!==|<=|>=|<<=|>>=|>>>=|<>|<|>|!|&&|\|\||\?\:|\*=|%=|\+=|\-=|&=|\^=/, 257 | next : "start" 258 | }, { 259 | token : "punctuation.operator", 260 | regex : /\?|\:|\,|\;|\./, 261 | next : "start" 262 | }, { 263 | token : "paren.lparen", 264 | regex : /[\[({]/, 265 | next : "start" 266 | }, { 267 | token : "paren.rparen", 268 | regex : /[\])}]/ 269 | }, { 270 | token : "keyword.operator", 271 | regex : /\/=?/, 272 | next : "start" 273 | }, { 274 | token: "comment", 275 | regex: /^#!.*$/ 276 | } 277 | ], 278 | "start": [ 279 | DocCommentHighlightRules.getStartRule("doc-start"), 280 | { 281 | token : "comment", // multi line comment 282 | regex : "\\/\\*", 283 | next : "comment_regex_allowed" 284 | }, { 285 | token : "comment", 286 | regex : "\\/\\/.*$", 287 | next : "start" 288 | }, { 289 | token: "string.regexp", 290 | regex: "\\/", 291 | next: "regex" 292 | }, { 293 | token : "text", 294 | regex : "\\s+|^$", 295 | next : "start" 296 | }, { 297 | token: "empty", 298 | regex: "", 299 | next: "no_regex" 300 | } 301 | ], 302 | "regex": [ 303 | { 304 | token: "regexp.keyword.operator", 305 | regex: "\\\\(?:u[\\da-fA-F]{4}|x[\\da-fA-F]{2}|.)" 306 | }, { 307 | token: "string.regexp", 308 | regex: "/\\w*", 309 | next: "no_regex" 310 | }, { 311 | token : "invalid", 312 | regex: /\{\d+\b,?\d*\}[+*]|[+*$^?][+*]|[$^][?]|\?{3,}/ 313 | }, { 314 | token : "constant.language.escape", 315 | regex: /\(\?[:=!]|\)|\{\d+\b,?\d*\}|[+*]\?|[()$^+*?]/ 316 | }, { 317 | token : "constant.language.delimiter", 318 | regex: /\|/ 319 | }, { 320 | token: "constant.language.escape", 321 | regex: /\[\^?/, 322 | next: "regex_character_class" 323 | }, { 324 | token: "empty", 325 | regex: "$", 326 | next: "no_regex" 327 | }, { 328 | defaultToken: "string.regexp" 329 | } 330 | ], 331 | "regex_character_class": [ 332 | { 333 | token: "regexp.keyword.operator", 334 | regex: "\\\\(?:u[\\da-fA-F]{4}|x[\\da-fA-F]{2}|.)" 335 | }, { 336 | token: "constant.language.escape", 337 | regex: "]", 338 | next: "regex" 339 | }, { 340 | token: "constant.language.escape", 341 | regex: "-" 342 | }, { 343 | token: "empty", 344 | regex: "$", 345 | next: "no_regex" 346 | }, { 347 | defaultToken: "string.regexp.charachterclass" 348 | } 349 | ], 350 | "function_arguments": [ 351 | { 352 | token: "variable.parameter", 353 | regex: identifierRe 354 | }, { 355 | token: "punctuation.operator", 356 | regex: "[, ]+" 357 | }, { 358 | token: "punctuation.operator", 359 | regex: "$" 360 | }, { 361 | token: "empty", 362 | regex: "", 363 | next: "no_regex" 364 | } 365 | ], 366 | "comment_regex_allowed" : [ 367 | {token : "comment", regex : "\\*\\/", next : "start"}, 368 | {defaultToken : "comment"} 369 | ], 370 | "comment" : [ 371 | {token : "comment", regex : "\\*\\/", next : "no_regex"}, 372 | {defaultToken : "comment"} 373 | ], 374 | "qqstring" : [ 375 | { 376 | token : "constant.language.escape", 377 | regex : escapedRe 378 | }, { 379 | token : "string", 380 | regex : "\\\\$", 381 | next : "qqstring" 382 | }, { 383 | token : "string", 384 | regex : '"|$', 385 | next : "no_regex" 386 | }, { 387 | defaultToken: "string" 388 | } 389 | ], 390 | "qstring" : [ 391 | { 392 | token : "constant.language.escape", 393 | regex : escapedRe 394 | }, { 395 | token : "string", 396 | regex : "\\\\$", 397 | next : "qstring" 398 | }, { 399 | token : "string", 400 | regex : "'|$", 401 | next : "no_regex" 402 | }, { 403 | defaultToken: "string" 404 | } 405 | ] 406 | }; 407 | 408 | this.embedRules(DocCommentHighlightRules, "doc-", 409 | [ DocCommentHighlightRules.getEndRule("no_regex") ]); 410 | }; 411 | 412 | oop.inherits(JavaScriptHighlightRules, TextHighlightRules); 413 | 414 | exports.JavaScriptHighlightRules = JavaScriptHighlightRules; 415 | }); 416 | 417 | ace.define('ace/mode/doc_comment_highlight_rules', ['require', 'exports', 'module' , 'ace/lib/oop', 'ace/mode/text_highlight_rules'], function(require, exports, module) { 418 | 419 | 420 | var oop = require("../lib/oop"); 421 | var TextHighlightRules = require("./text_highlight_rules").TextHighlightRules; 422 | 423 | var DocCommentHighlightRules = function() { 424 | 425 | this.$rules = { 426 | "start" : [ { 427 | token : "comment.doc.tag", 428 | regex : "@[\\w\\d_]+" // TODO: fix email addresses 429 | }, { 430 | token : "comment.doc.tag", 431 | regex : "\\bTODO\\b" 432 | }, { 433 | defaultToken : "comment.doc" 434 | }] 435 | }; 436 | }; 437 | 438 | oop.inherits(DocCommentHighlightRules, TextHighlightRules); 439 | 440 | DocCommentHighlightRules.getStartRule = function(start) { 441 | return { 442 | token : "comment.doc", // doc comment 443 | regex : "\\/\\*(?=\\*)", 444 | next : start 445 | }; 446 | }; 447 | 448 | DocCommentHighlightRules.getEndRule = function (start) { 449 | return { 450 | token : "comment.doc", // closing comment 451 | regex : "\\*\\/", 452 | next : start 453 | }; 454 | }; 455 | 456 | 457 | exports.DocCommentHighlightRules = DocCommentHighlightRules; 458 | 459 | }); 460 | 461 | ace.define('ace/mode/matching_brace_outdent', ['require', 'exports', 'module' , 'ace/range'], function(require, exports, module) { 462 | 463 | 464 | var Range = require("../range").Range; 465 | 466 | var MatchingBraceOutdent = function() {}; 467 | 468 | (function() { 469 | 470 | this.checkOutdent = function(line, input) { 471 | if (! /^\s+$/.test(line)) 472 | return false; 473 | 474 | return /^\s*\}/.test(input); 475 | }; 476 | 477 | this.autoOutdent = function(doc, row) { 478 | var line = doc.getLine(row); 479 | var match = line.match(/^(\s*\})/); 480 | 481 | if (!match) return 0; 482 | 483 | var column = match[1].length; 484 | var openBracePos = doc.findMatchingBracket({row: row, column: column}); 485 | 486 | if (!openBracePos || openBracePos.row == row) return 0; 487 | 488 | var indent = this.$getIndent(doc.getLine(openBracePos.row)); 489 | doc.replace(new Range(row, 0, row, column-1), indent); 490 | }; 491 | 492 | this.$getIndent = function(line) { 493 | return line.match(/^\s*/)[0]; 494 | }; 495 | 496 | }).call(MatchingBraceOutdent.prototype); 497 | 498 | exports.MatchingBraceOutdent = MatchingBraceOutdent; 499 | }); 500 | 501 | ace.define('ace/mode/behaviour/cstyle', ['require', 'exports', 'module' , 'ace/lib/oop', 'ace/mode/behaviour', 'ace/token_iterator', 'ace/lib/lang'], function(require, exports, module) { 502 | 503 | 504 | var oop = require("../../lib/oop"); 505 | var Behaviour = require("../behaviour").Behaviour; 506 | var TokenIterator = require("../../token_iterator").TokenIterator; 507 | var lang = require("../../lib/lang"); 508 | 509 | var SAFE_INSERT_IN_TOKENS = 510 | ["text", "paren.rparen", "punctuation.operator"]; 511 | var SAFE_INSERT_BEFORE_TOKENS = 512 | ["text", "paren.rparen", "punctuation.operator", "comment"]; 513 | 514 | 515 | var autoInsertedBrackets = 0; 516 | var autoInsertedRow = -1; 517 | var autoInsertedLineEnd = ""; 518 | var maybeInsertedBrackets = 0; 519 | var maybeInsertedRow = -1; 520 | var maybeInsertedLineStart = ""; 521 | var maybeInsertedLineEnd = ""; 522 | 523 | var CstyleBehaviour = function () { 524 | 525 | CstyleBehaviour.isSaneInsertion = function(editor, session) { 526 | var cursor = editor.getCursorPosition(); 527 | var iterator = new TokenIterator(session, cursor.row, cursor.column); 528 | if (!this.$matchTokenType(iterator.getCurrentToken() || "text", SAFE_INSERT_IN_TOKENS)) { 529 | var iterator2 = new TokenIterator(session, cursor.row, cursor.column + 1); 530 | if (!this.$matchTokenType(iterator2.getCurrentToken() || "text", SAFE_INSERT_IN_TOKENS)) 531 | return false; 532 | } 533 | iterator.stepForward(); 534 | return iterator.getCurrentTokenRow() !== cursor.row || 535 | this.$matchTokenType(iterator.getCurrentToken() || "text", SAFE_INSERT_BEFORE_TOKENS); 536 | }; 537 | 538 | CstyleBehaviour.$matchTokenType = function(token, types) { 539 | return types.indexOf(token.type || token) > -1; 540 | }; 541 | 542 | CstyleBehaviour.recordAutoInsert = function(editor, session, bracket) { 543 | var cursor = editor.getCursorPosition(); 544 | var line = session.doc.getLine(cursor.row); 545 | if (!this.isAutoInsertedClosing(cursor, line, autoInsertedLineEnd[0])) 546 | autoInsertedBrackets = 0; 547 | autoInsertedRow = cursor.row; 548 | autoInsertedLineEnd = bracket + line.substr(cursor.column); 549 | autoInsertedBrackets++; 550 | }; 551 | 552 | CstyleBehaviour.recordMaybeInsert = function(editor, session, bracket) { 553 | var cursor = editor.getCursorPosition(); 554 | var line = session.doc.getLine(cursor.row); 555 | if (!this.isMaybeInsertedClosing(cursor, line)) 556 | maybeInsertedBrackets = 0; 557 | maybeInsertedRow = cursor.row; 558 | maybeInsertedLineStart = line.substr(0, cursor.column) + bracket; 559 | maybeInsertedLineEnd = line.substr(cursor.column); 560 | maybeInsertedBrackets++; 561 | }; 562 | 563 | CstyleBehaviour.isAutoInsertedClosing = function(cursor, line, bracket) { 564 | return autoInsertedBrackets > 0 && 565 | cursor.row === autoInsertedRow && 566 | bracket === autoInsertedLineEnd[0] && 567 | line.substr(cursor.column) === autoInsertedLineEnd; 568 | }; 569 | 570 | CstyleBehaviour.isMaybeInsertedClosing = function(cursor, line) { 571 | return maybeInsertedBrackets > 0 && 572 | cursor.row === maybeInsertedRow && 573 | line.substr(cursor.column) === maybeInsertedLineEnd && 574 | line.substr(0, cursor.column) == maybeInsertedLineStart; 575 | }; 576 | 577 | CstyleBehaviour.popAutoInsertedClosing = function() { 578 | autoInsertedLineEnd = autoInsertedLineEnd.substr(1); 579 | autoInsertedBrackets--; 580 | }; 581 | 582 | CstyleBehaviour.clearMaybeInsertedClosing = function() { 583 | maybeInsertedBrackets = 0; 584 | maybeInsertedRow = -1; 585 | }; 586 | 587 | this.add("braces", "insertion", function (state, action, editor, session, text) { 588 | var cursor = editor.getCursorPosition(); 589 | var line = session.doc.getLine(cursor.row); 590 | if (text == '{') { 591 | var selection = editor.getSelectionRange(); 592 | var selected = session.doc.getTextRange(selection); 593 | if (selected !== "" && selected !== "{" && editor.getWrapBehavioursEnabled()) { 594 | return { 595 | text: '{' + selected + '}', 596 | selection: false 597 | }; 598 | } else if (CstyleBehaviour.isSaneInsertion(editor, session)) { 599 | if (/[\]\}\)]/.test(line[cursor.column])) { 600 | CstyleBehaviour.recordAutoInsert(editor, session, "}"); 601 | return { 602 | text: '{}', 603 | selection: [1, 1] 604 | }; 605 | } else { 606 | CstyleBehaviour.recordMaybeInsert(editor, session, "{"); 607 | return { 608 | text: '{', 609 | selection: [1, 1] 610 | }; 611 | } 612 | } 613 | } else if (text == '}') { 614 | var rightChar = line.substring(cursor.column, cursor.column + 1); 615 | if (rightChar == '}') { 616 | var matching = session.$findOpeningBracket('}', {column: cursor.column + 1, row: cursor.row}); 617 | if (matching !== null && CstyleBehaviour.isAutoInsertedClosing(cursor, line, text)) { 618 | CstyleBehaviour.popAutoInsertedClosing(); 619 | return { 620 | text: '', 621 | selection: [1, 1] 622 | }; 623 | } 624 | } 625 | } else if (text == "\n" || text == "\r\n") { 626 | var closing = ""; 627 | if (CstyleBehaviour.isMaybeInsertedClosing(cursor, line)) { 628 | closing = lang.stringRepeat("}", maybeInsertedBrackets); 629 | CstyleBehaviour.clearMaybeInsertedClosing(); 630 | } 631 | var rightChar = line.substring(cursor.column, cursor.column + 1); 632 | if (rightChar == '}' || closing !== "") { 633 | var openBracePos = session.findMatchingBracket({row: cursor.row, column: cursor.column}, '}'); 634 | if (!openBracePos) 635 | return null; 636 | 637 | var indent = this.getNextLineIndent(state, line.substring(0, cursor.column), session.getTabString()); 638 | var next_indent = this.$getIndent(line); 639 | 640 | return { 641 | text: '\n' + indent + '\n' + next_indent + closing, 642 | selection: [1, indent.length, 1, indent.length] 643 | }; 644 | } 645 | } 646 | }); 647 | 648 | this.add("braces", "deletion", function (state, action, editor, session, range) { 649 | var selected = session.doc.getTextRange(range); 650 | if (!range.isMultiLine() && selected == '{') { 651 | var line = session.doc.getLine(range.start.row); 652 | var rightChar = line.substring(range.end.column, range.end.column + 1); 653 | if (rightChar == '}') { 654 | range.end.column++; 655 | return range; 656 | } else { 657 | maybeInsertedBrackets--; 658 | } 659 | } 660 | }); 661 | 662 | this.add("parens", "insertion", function (state, action, editor, session, text) { 663 | if (text == '(') { 664 | var selection = editor.getSelectionRange(); 665 | var selected = session.doc.getTextRange(selection); 666 | if (selected !== "" && editor.getWrapBehavioursEnabled()) { 667 | return { 668 | text: '(' + selected + ')', 669 | selection: false 670 | }; 671 | } else if (CstyleBehaviour.isSaneInsertion(editor, session)) { 672 | CstyleBehaviour.recordAutoInsert(editor, session, ")"); 673 | return { 674 | text: '()', 675 | selection: [1, 1] 676 | }; 677 | } 678 | } else if (text == ')') { 679 | var cursor = editor.getCursorPosition(); 680 | var line = session.doc.getLine(cursor.row); 681 | var rightChar = line.substring(cursor.column, cursor.column + 1); 682 | if (rightChar == ')') { 683 | var matching = session.$findOpeningBracket(')', {column: cursor.column + 1, row: cursor.row}); 684 | if (matching !== null && CstyleBehaviour.isAutoInsertedClosing(cursor, line, text)) { 685 | CstyleBehaviour.popAutoInsertedClosing(); 686 | return { 687 | text: '', 688 | selection: [1, 1] 689 | }; 690 | } 691 | } 692 | } 693 | }); 694 | 695 | this.add("parens", "deletion", function (state, action, editor, session, range) { 696 | var selected = session.doc.getTextRange(range); 697 | if (!range.isMultiLine() && selected == '(') { 698 | var line = session.doc.getLine(range.start.row); 699 | var rightChar = line.substring(range.start.column + 1, range.start.column + 2); 700 | if (rightChar == ')') { 701 | range.end.column++; 702 | return range; 703 | } 704 | } 705 | }); 706 | 707 | this.add("brackets", "insertion", function (state, action, editor, session, text) { 708 | if (text == '[') { 709 | var selection = editor.getSelectionRange(); 710 | var selected = session.doc.getTextRange(selection); 711 | if (selected !== "" && editor.getWrapBehavioursEnabled()) { 712 | return { 713 | text: '[' + selected + ']', 714 | selection: false 715 | }; 716 | } else if (CstyleBehaviour.isSaneInsertion(editor, session)) { 717 | CstyleBehaviour.recordAutoInsert(editor, session, "]"); 718 | return { 719 | text: '[]', 720 | selection: [1, 1] 721 | }; 722 | } 723 | } else if (text == ']') { 724 | var cursor = editor.getCursorPosition(); 725 | var line = session.doc.getLine(cursor.row); 726 | var rightChar = line.substring(cursor.column, cursor.column + 1); 727 | if (rightChar == ']') { 728 | var matching = session.$findOpeningBracket(']', {column: cursor.column + 1, row: cursor.row}); 729 | if (matching !== null && CstyleBehaviour.isAutoInsertedClosing(cursor, line, text)) { 730 | CstyleBehaviour.popAutoInsertedClosing(); 731 | return { 732 | text: '', 733 | selection: [1, 1] 734 | }; 735 | } 736 | } 737 | } 738 | }); 739 | 740 | this.add("brackets", "deletion", function (state, action, editor, session, range) { 741 | var selected = session.doc.getTextRange(range); 742 | if (!range.isMultiLine() && selected == '[') { 743 | var line = session.doc.getLine(range.start.row); 744 | var rightChar = line.substring(range.start.column + 1, range.start.column + 2); 745 | if (rightChar == ']') { 746 | range.end.column++; 747 | return range; 748 | } 749 | } 750 | }); 751 | 752 | this.add("string_dquotes", "insertion", function (state, action, editor, session, text) { 753 | if (text == '"' || text == "'") { 754 | var quote = text; 755 | var selection = editor.getSelectionRange(); 756 | var selected = session.doc.getTextRange(selection); 757 | if (selected !== "" && selected !== "'" && selected != '"' && editor.getWrapBehavioursEnabled()) { 758 | return { 759 | text: quote + selected + quote, 760 | selection: false 761 | }; 762 | } else { 763 | var cursor = editor.getCursorPosition(); 764 | var line = session.doc.getLine(cursor.row); 765 | var leftChar = line.substring(cursor.column-1, cursor.column); 766 | if (leftChar == '\\') { 767 | return null; 768 | } 769 | var tokens = session.getTokens(selection.start.row); 770 | var col = 0, token; 771 | var quotepos = -1; // Track whether we're inside an open quote. 772 | 773 | for (var x = 0; x < tokens.length; x++) { 774 | token = tokens[x]; 775 | if (token.type == "string") { 776 | quotepos = -1; 777 | } else if (quotepos < 0) { 778 | quotepos = token.value.indexOf(quote); 779 | } 780 | if ((token.value.length + col) > selection.start.column) { 781 | break; 782 | } 783 | col += tokens[x].value.length; 784 | } 785 | if (!token || (quotepos < 0 && token.type !== "comment" && (token.type !== "string" || ((selection.start.column !== token.value.length+col-1) && token.value.lastIndexOf(quote) === token.value.length-1)))) { 786 | if (!CstyleBehaviour.isSaneInsertion(editor, session)) 787 | return; 788 | return { 789 | text: quote + quote, 790 | selection: [1,1] 791 | }; 792 | } else if (token && token.type === "string") { 793 | var rightChar = line.substring(cursor.column, cursor.column + 1); 794 | if (rightChar == quote) { 795 | return { 796 | text: '', 797 | selection: [1, 1] 798 | }; 799 | } 800 | } 801 | } 802 | } 803 | }); 804 | 805 | this.add("string_dquotes", "deletion", function (state, action, editor, session, range) { 806 | var selected = session.doc.getTextRange(range); 807 | if (!range.isMultiLine() && (selected == '"' || selected == "'")) { 808 | var line = session.doc.getLine(range.start.row); 809 | var rightChar = line.substring(range.start.column + 1, range.start.column + 2); 810 | if (rightChar == selected) { 811 | range.end.column++; 812 | return range; 813 | } 814 | } 815 | }); 816 | 817 | }; 818 | 819 | oop.inherits(CstyleBehaviour, Behaviour); 820 | 821 | exports.CstyleBehaviour = CstyleBehaviour; 822 | }); 823 | 824 | ace.define('ace/mode/folding/cstyle', ['require', 'exports', 'module' , 'ace/lib/oop', 'ace/range', 'ace/mode/folding/fold_mode'], function(require, exports, module) { 825 | 826 | 827 | var oop = require("../../lib/oop"); 828 | var Range = require("../../range").Range; 829 | var BaseFoldMode = require("./fold_mode").FoldMode; 830 | 831 | var FoldMode = exports.FoldMode = function(commentRegex) { 832 | if (commentRegex) { 833 | this.foldingStartMarker = new RegExp( 834 | this.foldingStartMarker.source.replace(/\|[^|]*?$/, "|" + commentRegex.start) 835 | ); 836 | this.foldingStopMarker = new RegExp( 837 | this.foldingStopMarker.source.replace(/\|[^|]*?$/, "|" + commentRegex.end) 838 | ); 839 | } 840 | }; 841 | oop.inherits(FoldMode, BaseFoldMode); 842 | 843 | (function() { 844 | 845 | this.foldingStartMarker = /(\{|\[)[^\}\]]*$|^\s*(\/\*)/; 846 | this.foldingStopMarker = /^[^\[\{]*(\}|\])|^[\s\*]*(\*\/)/; 847 | 848 | this.getFoldWidgetRange = function(session, foldStyle, row) { 849 | var line = session.getLine(row); 850 | var match = line.match(this.foldingStartMarker); 851 | if (match) { 852 | var i = match.index; 853 | 854 | if (match[1]) 855 | return this.openingBracketBlock(session, match[1], row, i); 856 | 857 | return session.getCommentFoldRange(row, i + match[0].length, 1); 858 | } 859 | 860 | if (foldStyle !== "markbeginend") 861 | return; 862 | 863 | var match = line.match(this.foldingStopMarker); 864 | if (match) { 865 | var i = match.index + match[0].length; 866 | 867 | if (match[1]) 868 | return this.closingBracketBlock(session, match[1], row, i); 869 | 870 | return session.getCommentFoldRange(row, i, -1); 871 | } 872 | }; 873 | 874 | }).call(FoldMode.prototype); 875 | 876 | }); 877 | -------------------------------------------------------------------------------- /external/qunit.css: -------------------------------------------------------------------------------- 1 | /** 2 | * QUnit v1.12.0 - A JavaScript Unit Testing Framework 3 | * 4 | * http://qunitjs.com 5 | * 6 | * Copyright 2012 jQuery Foundation and other contributors 7 | * Released under the MIT license. 8 | * http://jquery.org/license 9 | */ 10 | 11 | /** Font Family and Sizes */ 12 | 13 | #qunit-tests, #qunit-header, #qunit-banner, #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult { 14 | font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif; 15 | } 16 | 17 | #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult, #qunit-tests li { font-size: small; } 18 | #qunit-tests { font-size: smaller; } 19 | 20 | 21 | /** Resets */ 22 | 23 | #qunit-tests, #qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult, #qunit-modulefilter { 24 | margin: 0; 25 | padding: 0; 26 | } 27 | 28 | 29 | /** Header */ 30 | 31 | #qunit-header { 32 | padding: 0.5em 0 0.5em 1em; 33 | 34 | color: #8699a4; 35 | background-color: #0d3349; 36 | 37 | font-size: 1.5em; 38 | line-height: 1em; 39 | font-weight: normal; 40 | 41 | border-radius: 5px 5px 0 0; 42 | -moz-border-radius: 5px 5px 0 0; 43 | -webkit-border-top-right-radius: 5px; 44 | -webkit-border-top-left-radius: 5px; 45 | } 46 | 47 | #qunit-header a { 48 | text-decoration: none; 49 | color: #c2ccd1; 50 | } 51 | 52 | #qunit-header a:hover, 53 | #qunit-header a:focus { 54 | color: #fff; 55 | } 56 | 57 | #qunit-testrunner-toolbar label { 58 | display: inline-block; 59 | padding: 0 .5em 0 .1em; 60 | } 61 | 62 | #qunit-banner { 63 | height: 5px; 64 | } 65 | 66 | #qunit-testrunner-toolbar { 67 | padding: 0.5em 0 0.5em 2em; 68 | color: #5E740B; 69 | background-color: #eee; 70 | overflow: hidden; 71 | } 72 | 73 | #qunit-userAgent { 74 | padding: 0.5em 0 0.5em 2.5em; 75 | background-color: #2b81af; 76 | color: #fff; 77 | text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px; 78 | } 79 | 80 | #qunit-modulefilter-container { 81 | float: right; 82 | } 83 | 84 | /** Tests: Pass/Fail */ 85 | 86 | #qunit-tests { 87 | list-style-position: inside; 88 | } 89 | 90 | #qunit-tests li { 91 | padding: 0.4em 0.5em 0.4em 2.5em; 92 | border-bottom: 1px solid #fff; 93 | list-style-position: inside; 94 | } 95 | 96 | #qunit-tests.hidepass li.pass, #qunit-tests.hidepass li.running { 97 | display: none; 98 | } 99 | 100 | #qunit-tests li strong { 101 | cursor: pointer; 102 | } 103 | 104 | #qunit-tests li a { 105 | padding: 0.5em; 106 | color: #c2ccd1; 107 | text-decoration: none; 108 | } 109 | #qunit-tests li a:hover, 110 | #qunit-tests li a:focus { 111 | color: #000; 112 | } 113 | 114 | #qunit-tests li .runtime { 115 | float: right; 116 | font-size: smaller; 117 | } 118 | 119 | .qunit-assert-list { 120 | margin-top: 0.5em; 121 | padding: 0.5em; 122 | 123 | background-color: #fff; 124 | 125 | border-radius: 5px; 126 | -moz-border-radius: 5px; 127 | -webkit-border-radius: 5px; 128 | } 129 | 130 | .qunit-collapsed { 131 | display: none; 132 | } 133 | 134 | #qunit-tests table { 135 | border-collapse: collapse; 136 | margin-top: .2em; 137 | } 138 | 139 | #qunit-tests th { 140 | text-align: right; 141 | vertical-align: top; 142 | padding: 0 .5em 0 0; 143 | } 144 | 145 | #qunit-tests td { 146 | vertical-align: top; 147 | } 148 | 149 | #qunit-tests pre { 150 | margin: 0; 151 | white-space: pre-wrap; 152 | word-wrap: break-word; 153 | } 154 | 155 | #qunit-tests del { 156 | background-color: #e0f2be; 157 | color: #374e0c; 158 | text-decoration: none; 159 | } 160 | 161 | #qunit-tests ins { 162 | background-color: #ffcaca; 163 | color: #500; 164 | text-decoration: none; 165 | } 166 | 167 | /*** Test Counts */ 168 | 169 | #qunit-tests b.counts { color: black; } 170 | #qunit-tests b.passed { color: #5E740B; } 171 | #qunit-tests b.failed { color: #710909; } 172 | 173 | #qunit-tests li li { 174 | padding: 5px; 175 | background-color: #fff; 176 | border-bottom: none; 177 | list-style-position: inside; 178 | } 179 | 180 | /*** Passing Styles */ 181 | 182 | #qunit-tests li li.pass { 183 | color: #3c510c; 184 | background-color: #fff; 185 | border-left: 10px solid #C6E746; 186 | } 187 | 188 | #qunit-tests .pass { color: #528CE0; background-color: #D2E0E6; } 189 | #qunit-tests .pass .test-name { color: #366097; } 190 | 191 | #qunit-tests .pass .test-actual, 192 | #qunit-tests .pass .test-expected { color: #999999; } 193 | 194 | #qunit-banner.qunit-pass { background-color: #C6E746; } 195 | 196 | /*** Failing Styles */ 197 | 198 | #qunit-tests li li.fail { 199 | color: #710909; 200 | background-color: #fff; 201 | border-left: 10px solid #EE5757; 202 | white-space: pre; 203 | } 204 | 205 | #qunit-tests > li:last-child { 206 | border-radius: 0 0 5px 5px; 207 | -moz-border-radius: 0 0 5px 5px; 208 | -webkit-border-bottom-right-radius: 5px; 209 | -webkit-border-bottom-left-radius: 5px; 210 | } 211 | 212 | #qunit-tests .fail { color: #000000; background-color: #EE5757; } 213 | #qunit-tests .fail .test-name, 214 | #qunit-tests .fail .module-name { color: #000000; } 215 | 216 | #qunit-tests .fail .test-actual { color: #EE5757; } 217 | #qunit-tests .fail .test-expected { color: green; } 218 | 219 | #qunit-banner.qunit-fail { background-color: #EE5757; } 220 | 221 | 222 | /** Result */ 223 | 224 | #qunit-testresult { 225 | padding: 0.5em 0.5em 0.5em 2.5em; 226 | 227 | color: #2b81af; 228 | background-color: #D2E0E6; 229 | 230 | border-bottom: 1px solid white; 231 | } 232 | #qunit-testresult .module-name { 233 | font-weight: bold; 234 | } 235 | 236 | /** Fixture */ 237 | 238 | #qunit-fixture { 239 | position: absolute; 240 | top: -10000px; 241 | left: -10000px; 242 | width: 1000px; 243 | height: 1000px; 244 | } -------------------------------------------------------------------------------- /external/rainbow/language/generic.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Generic language patterns 3 | * 4 | * @author Craig Campbell 5 | * @version 1.0.10 6 | */ 7 | Rainbow.extend([ 8 | { 9 | 'matches': { 10 | 1: { 11 | 'name': 'keyword.operator', 12 | 'pattern': /\=/g 13 | }, 14 | 2: { 15 | 'name': 'string', 16 | 'matches': { 17 | 'name': 'constant.character.escape', 18 | 'pattern': /\\('|"){1}/g 19 | } 20 | } 21 | }, 22 | 'pattern': /(\(|\s|\[|\=|:)(('|")([^\\\1]|\\.)*?(\3))/gm 23 | }, 24 | { 25 | 'name': 'comment', 26 | 'pattern': /\/\*[\s\S]*?\*\/|(\/\/|\#)[\s\S]*?$/gm 27 | }, 28 | { 29 | 'name': 'constant.numeric', 30 | 'pattern': /\b(\d+(\.\d+)?(e(\+|\-)?\d+)?(f|d)?|0x[\da-f]+)\b/gi 31 | }, 32 | { 33 | 'matches': { 34 | 1: 'keyword' 35 | }, 36 | 'pattern': /\b(and|array|as|b(ool(ean)?|reak)|c(ase|atch|har|lass|on(st|tinue))|d(ef|elete|o(uble)?)|e(cho|lse(if)?|xit|xtends|xcept)|f(inally|loat|or(each)?|unction)|global|if|import|int(eger)?|long|new|object|or|pr(int|ivate|otected)|public|return|self|st(ring|ruct|atic)|switch|th(en|is|row)|try|(un)?signed|var|void|while)(?=\(|\b)/gi 37 | }, 38 | { 39 | 'name': 'constant.language', 40 | 'pattern': /true|false|null/g 41 | }, 42 | { 43 | 'name': 'keyword.operator', 44 | 'pattern': /\+|\!|\-|&(gt|lt|amp);|\||\*|\=/g 45 | }, 46 | { 47 | 'matches': { 48 | 1: 'function.call' 49 | }, 50 | 'pattern': /(\w+?)(?=\()/g 51 | }, 52 | { 53 | 'matches': { 54 | 1: 'storage.function', 55 | 2: 'entity.name.function' 56 | }, 57 | 'pattern': /(function)\s(.*?)(?=\()/g 58 | } 59 | ]); 60 | -------------------------------------------------------------------------------- /external/rainbow/language/javascript.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Javascript patterns 3 | * 4 | * @author Craig Campbell 5 | * @version 1.0.9 6 | */ 7 | Rainbow.extend('javascript', [ 8 | 9 | /** 10 | * matches $. or $( 11 | */ 12 | { 13 | 'name': 'selector', 14 | 'pattern': /(\s|^)\$(?=\.|\()/g 15 | }, 16 | { 17 | 'name': 'support', 18 | 'pattern': /\b(window|document)\b/g 19 | }, 20 | { 21 | 'matches': { 22 | 1: 'support.property' 23 | }, 24 | 'pattern': /\.(length|node(Name|Value))\b/g 25 | }, 26 | { 27 | 'matches': { 28 | 1: 'support.function' 29 | }, 30 | 'pattern': /(setTimeout|setInterval)(?=\()/g 31 | 32 | }, 33 | { 34 | 'matches': { 35 | 1: 'support.method' 36 | }, 37 | 'pattern': /\.(getAttribute|push|getElementById|getElementsByClassName|log|setTimeout|setInterval)(?=\()/g 38 | }, 39 | 40 | /** 41 | * matches any escaped characters inside of a js regex pattern 42 | * 43 | * @see https://github.com/ccampbell/rainbow/issues/22 44 | * 45 | * this was causing single line comments to fail so it now makes sure 46 | * the opening / is not directly followed by a * 47 | * 48 | * @todo check that there is valid regex in match group 1 49 | */ 50 | { 51 | 'name': 'string.regexp', 52 | 'matches': { 53 | 1: 'string.regexp.open', 54 | 2: { 55 | 'name': 'constant.regexp.escape', 56 | 'pattern': /\\(.){1}/g 57 | }, 58 | 3: 'string.regexp.close', 59 | 4: 'string.regexp.modifier' 60 | }, 61 | 'pattern': /(\/)(?!\*)(.+)(\/)([igm]{0,3})/g 62 | }, 63 | 64 | /** 65 | * matches runtime function declarations 66 | */ 67 | { 68 | 'matches': { 69 | 1: 'storage', 70 | 3: 'entity.function' 71 | }, 72 | 'pattern': /(var)?(\s|^)(\S*)(?=\s?=\s?function\()/g 73 | }, 74 | 75 | /** 76 | * matches constructor call 77 | */ 78 | { 79 | 'matches': { 80 | 1: 'keyword', 81 | 2: 'entity.function' 82 | }, 83 | 'pattern': /(new)\s+(.*)(?=\()/g 84 | }, 85 | 86 | /** 87 | * matches any function call in the style functionName: function() 88 | */ 89 | { 90 | 'name': 'entity.function', 91 | 'pattern': /(\w+)(?=:\s{0,}function)/g 92 | } 93 | ]); 94 | -------------------------------------------------------------------------------- /external/rainbow/rainbow.min.js: -------------------------------------------------------------------------------- 1 | /* Rainbow v1.1.9 rainbowco.de */ 2 | window.Rainbow=function(){function q(a){var b,c=a.getAttribute&&a.getAttribute("data-language")||0;if(!c){a=a.attributes;for(b=0;b=e[d][c])delete e[d][c],delete j[d][c];if(a>=c&&ac&&b'+b+""}function s(a,b,c,h){var f=a.exec(c);if(f){++t;!b.name&&"string"==typeof b.matches[0]&&(b.name=b.matches[0],delete b.matches[0]);var k=f[0],i=f.index,u=f[0].length+i,g=function(){function f(){s(a,b,c,h)}t%100>0?f():setTimeout(f,0)};if(C(i,u))g();else{var m=v(b.matches),l=function(a,c,h){if(a>=c.length)h(k);else{var d=f[c[a]];if(d){var e=b.matches[c[a]],i=e.language,g=e.name&&e.matches? 4 | e.matches:e,j=function(b,d,e){var i;i=0;var g;for(g=1;g/g,">").replace(/&(?![\w\#]+;)/g, 6 | "&"),b,c)}function o(a,b,c){if(bu;u++)if(t.call(e,n[u],u,n)===r)return}else for(var a in n)if(j.has(n,a)&&t.call(e,n[a],a,n)===r)return};j.map=j.collect=function(n,t,r){var e=[];return null==n?e:p&&n.map===p?n.map(t,r):(A(n,function(n,u,i){e.push(t.call(r,n,u,i))}),e)};var E="Reduce of empty array with no initial value";j.reduce=j.foldl=j.inject=function(n,t,r,e){var u=arguments.length>2;if(null==n&&(n=[]),v&&n.reduce===v)return e&&(t=j.bind(t,e)),u?n.reduce(t,r):n.reduce(t);if(A(n,function(n,i,a){u?r=t.call(e,r,n,i,a):(r=n,u=!0)}),!u)throw new TypeError(E);return r},j.reduceRight=j.foldr=function(n,t,r,e){var u=arguments.length>2;if(null==n&&(n=[]),h&&n.reduceRight===h)return e&&(t=j.bind(t,e)),u?n.reduceRight(t,r):n.reduceRight(t);var i=n.length;if(i!==+i){var a=j.keys(n);i=a.length}if(A(n,function(o,c,l){c=a?a[--i]:--i,u?r=t.call(e,r,n[c],c,l):(r=n[c],u=!0)}),!u)throw new TypeError(E);return r},j.find=j.detect=function(n,t,r){var e;return O(n,function(n,u,i){return t.call(r,n,u,i)?(e=n,!0):void 0}),e},j.filter=j.select=function(n,t,r){var e=[];return null==n?e:d&&n.filter===d?n.filter(t,r):(A(n,function(n,u,i){t.call(r,n,u,i)&&e.push(n)}),e)},j.reject=function(n,t,r){return j.filter(n,function(n,e,u){return!t.call(r,n,e,u)},r)},j.every=j.all=function(n,t,e){t||(t=j.identity);var u=!0;return null==n?u:g&&n.every===g?n.every(t,e):(A(n,function(n,i,a){return(u=u&&t.call(e,n,i,a))?void 0:r}),!!u)};var O=j.some=j.any=function(n,t,e){t||(t=j.identity);var u=!1;return null==n?u:m&&n.some===m?n.some(t,e):(A(n,function(n,i,a){return u||(u=t.call(e,n,i,a))?r:void 0}),!!u)};j.contains=j.include=function(n,t){return null==n?!1:y&&n.indexOf===y?n.indexOf(t)!=-1:O(n,function(n){return n===t})},j.invoke=function(n,t){var r=o.call(arguments,2),e=j.isFunction(t);return j.map(n,function(n){return(e?t:n[t]).apply(n,r)})},j.pluck=function(n,t){return j.map(n,function(n){return n[t]})},j.where=function(n,t,r){return j.isEmpty(t)?r?void 0:[]:j[r?"find":"filter"](n,function(n){for(var r in t)if(t[r]!==n[r])return!1;return!0})},j.findWhere=function(n,t){return j.where(n,t,!0)},j.max=function(n,t,r){if(!t&&j.isArray(n)&&n[0]===+n[0]&&n.length<65535)return Math.max.apply(Math,n);if(!t&&j.isEmpty(n))return-1/0;var e={computed:-1/0,value:-1/0};return A(n,function(n,u,i){var a=t?t.call(r,n,u,i):n;a>e.computed&&(e={value:n,computed:a})}),e.value},j.min=function(n,t,r){if(!t&&j.isArray(n)&&n[0]===+n[0]&&n.length<65535)return Math.min.apply(Math,n);if(!t&&j.isEmpty(n))return 1/0;var e={computed:1/0,value:1/0};return A(n,function(n,u,i){var a=t?t.call(r,n,u,i):n;ae||r===void 0)return 1;if(e>r||e===void 0)return-1}return n.indexi;){var o=i+a>>>1;r.call(e,n[o])=0})})},j.difference=function(n){var t=c.apply(e,o.call(arguments,1));return j.filter(n,function(n){return!j.contains(t,n)})},j.zip=function(){for(var n=j.max(j.pluck(arguments,"length").concat(0)),t=new Array(n),r=0;n>r;r++)t[r]=j.pluck(arguments,""+r);return t},j.object=function(n,t){if(null==n)return{};for(var r={},e=0,u=n.length;u>e;e++)t?r[n[e]]=t[e]:r[n[e][0]]=n[e][1];return r},j.indexOf=function(n,t,r){if(null==n)return-1;var e=0,u=n.length;if(r){if("number"!=typeof r)return e=j.sortedIndex(n,t),n[e]===t?e:-1;e=0>r?Math.max(0,u+r):r}if(y&&n.indexOf===y)return n.indexOf(t,r);for(;u>e;e++)if(n[e]===t)return e;return-1},j.lastIndexOf=function(n,t,r){if(null==n)return-1;var e=null!=r;if(b&&n.lastIndexOf===b)return e?n.lastIndexOf(t,r):n.lastIndexOf(t);for(var u=e?r:n.length;u--;)if(n[u]===t)return u;return-1},j.range=function(n,t,r){arguments.length<=1&&(t=n||0,n=0),r=arguments[2]||1;for(var e=Math.max(Math.ceil((t-n)/r),0),u=0,i=new Array(e);e>u;)i[u++]=n,n+=r;return i};var M=function(){};j.bind=function(n,t){var r,e;if(w&&n.bind===w)return w.apply(n,o.call(arguments,1));if(!j.isFunction(n))throw new TypeError;return r=o.call(arguments,2),e=function(){if(!(this instanceof e))return n.apply(t,r.concat(o.call(arguments)));M.prototype=n.prototype;var u=new M;M.prototype=null;var i=n.apply(u,r.concat(o.call(arguments)));return Object(i)===i?i:u}},j.partial=function(n){var t=o.call(arguments,1);return function(){return n.apply(this,t.concat(o.call(arguments)))}},j.bindAll=function(n){var t=o.call(arguments,1);if(0===t.length)throw new Error("bindAll must be passed function names");return A(t,function(t){n[t]=j.bind(n[t],n)}),n},j.memoize=function(n,t){var r={};return t||(t=j.identity),function(){var e=t.apply(this,arguments);return j.has(r,e)?r[e]:r[e]=n.apply(this,arguments)}},j.delay=function(n,t){var r=o.call(arguments,2);return setTimeout(function(){return n.apply(null,r)},t)},j.defer=function(n){return j.delay.apply(j,[n,1].concat(o.call(arguments,1)))},j.throttle=function(n,t,r){var e,u,i,a=null,o=0;r||(r={});var c=function(){o=r.leading===!1?0:new Date,a=null,i=n.apply(e,u)};return function(){var l=new Date;o||r.leading!==!1||(o=l);var f=t-(l-o);return e=this,u=arguments,0>=f?(clearTimeout(a),a=null,o=l,i=n.apply(e,u)):a||r.trailing===!1||(a=setTimeout(c,f)),i}},j.debounce=function(n,t,r){var e,u=null;return function(){var i=this,a=arguments,o=function(){u=null,r||(e=n.apply(i,a))},c=r&&!u;return clearTimeout(u),u=setTimeout(o,t),c&&(e=n.apply(i,a)),e}},j.once=function(n){var t,r=!1;return function(){return r?t:(r=!0,t=n.apply(this,arguments),n=null,t)}},j.wrap=function(n,t){return function(){var r=[n];return a.apply(r,arguments),t.apply(this,r)}},j.compose=function(){var n=arguments;return function(){for(var t=arguments,r=n.length-1;r>=0;r--)t=[n[r].apply(this,t)];return t[0]}},j.after=function(n,t){return function(){return--n<1?t.apply(this,arguments):void 0}},j.keys=_||function(n){if(n!==Object(n))throw new TypeError("Invalid object");var t=[];for(var r in n)j.has(n,r)&&t.push(r);return t},j.values=function(n){var t=[];for(var r in n)j.has(n,r)&&t.push(n[r]);return t},j.pairs=function(n){var t=[];for(var r in n)j.has(n,r)&&t.push([r,n[r]]);return t},j.invert=function(n){var t={};for(var r in n)j.has(n,r)&&(t[n[r]]=r);return t},j.functions=j.methods=function(n){var t=[];for(var r in n)j.isFunction(n[r])&&t.push(r);return t.sort()},j.extend=function(n){return A(o.call(arguments,1),function(t){if(t)for(var r in t)n[r]=t[r]}),n},j.pick=function(n){var t={},r=c.apply(e,o.call(arguments,1));return A(r,function(r){r in n&&(t[r]=n[r])}),t},j.omit=function(n){var t={},r=c.apply(e,o.call(arguments,1));for(var u in n)j.contains(r,u)||(t[u]=n[u]);return t},j.defaults=function(n){return A(o.call(arguments,1),function(t){if(t)for(var r in t)n[r]===void 0&&(n[r]=t[r])}),n},j.clone=function(n){return j.isObject(n)?j.isArray(n)?n.slice():j.extend({},n):n},j.tap=function(n,t){return t(n),n};var S=function(n,t,r,e){if(n===t)return 0!==n||1/n==1/t;if(null==n||null==t)return n===t;n instanceof j&&(n=n._wrapped),t instanceof j&&(t=t._wrapped);var u=l.call(n);if(u!=l.call(t))return!1;switch(u){case"[object String]":return n==String(t);case"[object Number]":return n!=+n?t!=+t:0==n?1/n==1/t:n==+t;case"[object Date]":case"[object Boolean]":return+n==+t;case"[object RegExp]":return n.source==t.source&&n.global==t.global&&n.multiline==t.multiline&&n.ignoreCase==t.ignoreCase}if("object"!=typeof n||"object"!=typeof t)return!1;for(var i=r.length;i--;)if(r[i]==n)return e[i]==t;var a=n.constructor,o=t.constructor;if(a!==o&&!(j.isFunction(a)&&a instanceof a&&j.isFunction(o)&&o instanceof o))return!1;r.push(n),e.push(t);var c=0,f=!0;if("[object Array]"==u){if(c=n.length,f=c==t.length)for(;c--&&(f=S(n[c],t[c],r,e)););}else{for(var s in n)if(j.has(n,s)&&(c++,!(f=j.has(t,s)&&S(n[s],t[s],r,e))))break;if(f){for(s in t)if(j.has(t,s)&&!c--)break;f=!c}}return r.pop(),e.pop(),f};j.isEqual=function(n,t){return S(n,t,[],[])},j.isEmpty=function(n){if(null==n)return!0;if(j.isArray(n)||j.isString(n))return 0===n.length;for(var t in n)if(j.has(n,t))return!1;return!0},j.isElement=function(n){return!(!n||1!==n.nodeType)},j.isArray=x||function(n){return"[object Array]"==l.call(n)},j.isObject=function(n){return n===Object(n)},A(["Arguments","Function","String","Number","Date","RegExp"],function(n){j["is"+n]=function(t){return l.call(t)=="[object "+n+"]"}}),j.isArguments(arguments)||(j.isArguments=function(n){return!(!n||!j.has(n,"callee"))}),"function"!=typeof/./&&(j.isFunction=function(n){return"function"==typeof n}),j.isFinite=function(n){return isFinite(n)&&!isNaN(parseFloat(n))},j.isNaN=function(n){return j.isNumber(n)&&n!=+n},j.isBoolean=function(n){return n===!0||n===!1||"[object Boolean]"==l.call(n)},j.isNull=function(n){return null===n},j.isUndefined=function(n){return n===void 0},j.has=function(n,t){return f.call(n,t)},j.noConflict=function(){return n._=t,this},j.identity=function(n){return n},j.times=function(n,t,r){for(var e=Array(Math.max(0,n)),u=0;n>u;u++)e[u]=t.call(r,u);return e},j.random=function(n,t){return null==t&&(t=n,n=0),n+Math.floor(Math.random()*(t-n+1))};var I={escape:{"&":"&","<":"<",">":">",'"':""","'":"'","/":"/"}};I.unescape=j.invert(I.escape);var T={escape:new RegExp("["+j.keys(I.escape).join("")+"]","g"),unescape:new RegExp("("+j.keys(I.unescape).join("|")+")","g")};j.each(["escape","unescape"],function(n){j[n]=function(t){return null==t?"":(""+t).replace(T[n],function(t){return I[n][t]})}}),j.result=function(n,t){if(null==n)return void 0;var r=n[t];return j.isFunction(r)?r.call(n):r},j.mixin=function(n){A(j.functions(n),function(t){var r=j[t]=n[t];j.prototype[t]=function(){var n=[this._wrapped];return a.apply(n,arguments),z.call(this,r.apply(j,n))}})};var N=0;j.uniqueId=function(n){var t=++N+"";return n?n+t:t},j.templateSettings={evaluate:/<%([\s\S]+?)%>/g,interpolate:/<%=([\s\S]+?)%>/g,escape:/<%-([\s\S]+?)%>/g};var q=/(.)^/,B={"'":"'","\\":"\\","\r":"r","\n":"n"," ":"t","\u2028":"u2028","\u2029":"u2029"},D=/\\|'|\r|\n|\t|\u2028|\u2029/g;j.template=function(n,t,r){var e;r=j.defaults({},r,j.templateSettings);var u=new RegExp([(r.escape||q).source,(r.interpolate||q).source,(r.evaluate||q).source].join("|")+"|$","g"),i=0,a="__p+='";n.replace(u,function(t,r,e,u,o){return a+=n.slice(i,o).replace(D,function(n){return"\\"+B[n]}),r&&(a+="'+\n((__t=("+r+"))==null?'':_.escape(__t))+\n'"),e&&(a+="'+\n((__t=("+e+"))==null?'':__t)+\n'"),u&&(a+="';\n"+u+"\n__p+='"),i=o+t.length,t}),a+="';\n",r.variable||(a="with(obj||{}){\n"+a+"}\n"),a="var __t,__p='',__j=Array.prototype.join,"+"print=function(){__p+=__j.call(arguments,'');};\n"+a+"return __p;\n";try{e=new Function(r.variable||"obj","_",a)}catch(o){throw o.source=a,o}if(t)return e(t,j);var c=function(n){return e.call(this,n,j)};return c.source="function("+(r.variable||"obj")+"){\n"+a+"}",c},j.chain=function(n){return j(n).chain()};var z=function(n){return this._chain?j(n).chain():n};j.mixin(j),A(["pop","push","reverse","shift","sort","splice","unshift"],function(n){var t=e[n];j.prototype[n]=function(){var r=this._wrapped;return t.apply(r,arguments),"shift"!=n&&"splice"!=n||0!==r.length||delete r[0],z.call(this,r)}}),A(["concat","join","slice"],function(n){var t=e[n];j.prototype[n]=function(){return z.call(this,t.apply(this._wrapped,arguments))}}),j.extend(j.prototype,{chain:function(){return this._chain=!0,this},value:function(){return this._wrapped}})}.call(this); 6 | //# sourceMappingURL=underscore-min.map -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | StructuredJS Demo 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 |
StructuredJS
19 |
20 |

structured.js is a Javascript library that provides a simple interface for checking the structure of Javascript code, backed by the abstract syntax tree generated by Esprima. Structured.js works in-browser or as a standalone npm module. It is used particularly for checking beginner code to provide useful feedback as part of Khan Academy's CS curriculum.

21 | 22 |

A structure is valid Javascript code that contains blanks ( _ characters) and stand-ins ($str) that match values. The structure is sensitive to nesting and ordering. The matcher only requires that code contain the structure -- extra code has no effect.

23 | 24 |

Structures can contain any Javascript -- control structures (for, while, if, else, try, etc), function calls, variable declarations, parameters, numbers, strings, arrays, objects, and even operators are all fair game. Use _ characters to indicate what can be filled by anything. Use variables starting with $ to indicate what static values should be the same (i.e. $foo to indicate some number, string, or name). You can also check out the pretty display of structures.

25 | 26 |

Try it out!

27 |
28 | 29 |
30 | Required structure 31 | Code to check 32 |
33 |
if (_) { 34 | _ += _; 35 | for (var $a = $start; $a < $check; $a += _) { 36 | rect($a, $bigNum, 30, 30); 37 | _(); 38 | } 39 | }
40 | 41 |
if (y > 30 && x > 13) { 42 | x += y; 43 | for (var i = 0; i < 99; i += 1) { 44 | rect(i, 300, 30, 30); 45 | bar(); 46 | x = i + 2; 47 | } 48 | }
49 | 50 |
51 | Variable callbacks (optional) 52 | show/hide 53 |
54 |
// Optional extra functionality for variables. 55 | // Uncomment this code -- select it then cmd-/ or ctrl-/ 56 | { 57 | // "$start, $check": function(start, check) { 58 | // if (check.value < start.value) { 59 | // return {"failure": "Double-check your for loop!"} 60 | // } 61 | // return true; 62 | // }, 63 | // "$bigNum": function(bigNum) { 64 | // if (bigNum.value < 1000) { 65 | // return {"failure": "Your number is not big enough! " + bigNum.value} 66 | // } 67 | // return true; 68 | // } 69 | } 70 |
71 | 72 | 73 |
74 | 75 |
76 | (Match result here) 77 |
78 |
79 | (Fail message here) 80 |
81 |
82 | 83 |
84 | generate qunit test 85 |
86 |
87 |               QUnit test code generated here.
88 |           
89 |
90 |
91 |
92 | 93 | 94 | 95 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "structured", 3 | "version": "0.1.2", 4 | "description": "Simple interface for checking structure of JS code against a template, backed by Esprima.", 5 | "main": "structured.js", 6 | "scripts": { 7 | "test": "node testrunner" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git://github.com/Khan/structuredjs.git" 12 | }, 13 | "keywords": [ 14 | "parsing", 15 | "analysis", 16 | "ast", 17 | "checker", 18 | "structure" 19 | ], 20 | "author": "swestwood", 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/Khan/structuredjs/issues" 24 | }, 25 | "dependencies": { 26 | "esprima": "~1.0.3", 27 | "underscore": "~1.5.1" 28 | }, 29 | "devDependencies": { 30 | "qunit": "~0.5.16" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /pretty-display/display-demo.css: -------------------------------------------------------------------------------- 1 | /* CSS for the pretty display demo page. */ 2 | body { 3 | background-color: white; 4 | font-family: Helvetica; 5 | margin: 30px 0px; 6 | padding:0px; 7 | text-align:center; 8 | } 9 | #container { 10 | width:1000px; 11 | margin:0px auto; 12 | text-align:left; 13 | } 14 | .code-editor { 15 | display: inline-block; 16 | height: 300px; 17 | width: 400px; 18 | border: 1px solid gray; 19 | margin-top: .6em; 20 | margin-left: 1em; 21 | } 22 | #structure { 23 | margin-right: 2em; 24 | } 25 | 26 | #pretty-display { 27 | display: inline-block; 28 | vertical-align: top; 29 | width: 500px; 30 | margin-top: .6em; 31 | } 32 | 33 | .editor-labels { 34 | text-transform: lowercase; 35 | font-size: .9em; 36 | font-weight: 400; 37 | color: #666; 38 | letter-spacing: 1px; 39 | } 40 | .structure-label { 41 | margin-left: 30px; 42 | 43 | } 44 | .output-label { 45 | margin-left: 400px; 46 | } 47 | 48 | .page-label { 49 | text-align: center; 50 | font-family: helvetica; 51 | letter-spacing: .2em; 52 | text-transform: lowercase; 53 | font-variant: small-caps; 54 | color: rgb(223, 252, 255); 55 | font-size: 2.4em; 56 | padding-bottom:.2em; 57 | margin-bottom: .7em; 58 | color: teal; 59 | border-bottom: 1px solid lightslategray; 60 | } 61 | 62 | footer { 63 | font-size: .8em; 64 | margin-top: 50px; 65 | color: #999; 66 | text-align: right; 67 | padding-right: 100px; 68 | } 69 | 70 | a, a:visited { 71 | color: #22A; 72 | text-decoration: none; 73 | } 74 | a:hover { 75 | color: #AAA; 76 | } -------------------------------------------------------------------------------- /pretty-display/display.css: -------------------------------------------------------------------------------- 1 | /* CSS for nice formatting of structuredjs blanks and variables. */ 2 | pre.rainbowjs span.structuredjs_blank { 3 | border-bottom: 1px dashed gray; 4 | padding-left: 15px; 5 | padding-right: 15px; 6 | color: lightgray; 7 | line-height: 2; 8 | } 9 | pre.rainbowjs span.structuredjs_var { 10 | border: 1px dotted darkgray; 11 | padding-left: 15px; 12 | padding-right: 15px; 13 | background-color: white; 14 | color: white; 15 | line-height: 2; 16 | } 17 | pre.rainbowjs span.structuredjs_var.one { 18 | background-color: rgb(250, 222, 199); 19 | border-color: darkorange; 20 | } 21 | pre.rainbowjs span.structuredjs_var.two { 22 | background-color: rgb(248, 232, 248); 23 | border: 1px dotted rgb(238, 130, 238); 24 | } 25 | pre.rainbowjs span.structuredjs_var.three { 26 | background-color: rgb(209, 217, 248); 27 | border-color: rgb(142, 142, 202); 28 | } 29 | pre.rainbowjs span.structuredjs_var.four { 30 | background-color: rgb(252, 204, 204); 31 | border-color: salmon; 32 | } 33 | pre.rainbowjs span.structuredjs_var.five { 34 | background-color: rgb(182, 255, 185); 35 | border-color: rgb(79, 189, 57); 36 | } 37 | pre.rainbowjs span.structuredjs_var.six { 38 | background-color: rgb(207, 255, 253); 39 | border-color: rgb(42, 174, 199); 40 | } 41 | pre.rainbowjs span.structuredjs_var.seven { 42 | background-color: rgb(253, 242, 162); 43 | border-color: rgb(172, 166, 99); 44 | } -------------------------------------------------------------------------------- /pretty-display/display.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Takes the structure created in the code editor and generates pretty 3 | * HTML/CSS with the StructuredJS variables and blanks contained in special 4 | * spans. 5 | */ 6 | 7 | $(document).ready(function() { 8 | // Set up the editor 9 | var editor = ace.edit("structure"); 10 | setupEditor(editor); 11 | // Output results on the initial load. 12 | showPrettyCode(editor.getValue()); 13 | }); 14 | 15 | function setupEditor(editor) { 16 | editor.getSession().setUseWorker(false); 17 | editor.getSession().setMode("ace/mode/javascript"); 18 | editor.renderer.setShowGutter(false); 19 | editor.renderer.setPadding(6); 20 | editor.getSession().on('change', function(e) { 21 | showPrettyCode(editor.getValue()); 22 | }); 23 | } 24 | 25 | function showPrettyCode(code) { 26 | Structured.prettify(code, function(pretty) { 27 | $("#pretty-display").html(pretty); 28 | }); 29 | } -------------------------------------------------------------------------------- /pretty-display/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | StructuredJS Pretty Display Demo 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 |
StructuredJS Pretty Display
24 |
25 |

A pretty HTML display for 26 | structured.js 27 | structures that nicely displays blanks (_ characters) and up to seven variables ($someText), with 28 | syntax highlighting for the remaining Javascript code.

29 |

To match code using StructuredJS, go to the main demo.

30 |
31 | 32 |
33 | Structure 34 | Pretty 35 |
36 | 37 |
$foo = fn(_); 38 | if (_) { 39 | _ += _; 40 | for (var $a = _; $a < $b; $a += _) { 41 | rect($a, $b, 30, 30); 42 | _(); 43 | } 44 | $foo = $bar($baz + _); 45 | $bar($foo); 46 | }
47 | 48 |
The pretty code goes here.
49 | 50 |
51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /pretty-display/textmate-lite.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Textmate-like theme ported from Ace Editor. 3 | * 4 | * Only affects code within
 tags.
 5 |  *
 6 |  * @author Sophia Westwood
 7 |  * @version 1.0.0
 8 |  */
 9 | pre.rainbowjs {
10 |     background: #FFF;
11 |     word-wrap: break-word;
12 |     margin: 0px;
13 |     padding: 10px;
14 |     color: #000;
15 |     font-size: 12px;
16 |     margin-bottom: 20px;
17 |     line-height: 16px;
18 | }
19 | 
20 | pre.rainbowjs, code.rainbowjs {
21 |     font-family: 'Monaco', 'Consolas', monospace;
22 | }
23 | 
24 | pre.rainbowjs .comment {
25 |     color: rgb(76, 136, 107)
26 | }
27 | 
28 | pre.rainbowjs .constant {
29 |     color: rgb(197, 6, 11);
30 | }
31 | 
32 | pre.rainbowjs .constant.numeric {
33 |     color: rgb(0, 0, 205);
34 | }
35 | 
36 | pre.rainbowjs .storage {
37 |     color: rgb(0, 0, 255);
38 | }
39 | 
40 | pre.rainbowjs .string {
41 |     color: rgb(3, 106, 7);
42 | }
43 | 
44 | pre.rainbowjs .keyword, pre.rainbowjs .selector {
45 |     color: blue;
46 | }
47 | 
48 | pre.rainbowjs .keyword.operator {
49 |     color: rgb(104, 118, 135);
50 | }
51 | 
52 | pre.rainbowjs .inherited-class {
53 |     font-style: italic;
54 | }
55 | 
56 | pre.rainbowjs .entity {
57 |     color: #3E853F;
58 | }
59 | 
60 | pre.rainbowjs .entity.name.function {
61 |     color: #0000A2;
62 | }
63 | 
64 | pre.rainbowjs .support {
65 |     color: #192140;
66 | }
67 | 
68 | pre.rainbowjs .variable.global, pre.rainbowjs .variable.class, pre.rainbowjs .variable.instance {
69 |     color: rgb(49, 132, 149);
70 | }


--------------------------------------------------------------------------------
/structured.js:
--------------------------------------------------------------------------------
   1 | /*
   2 |  * StructuredJS provides an API for static analysis of code based on an abstract
   3 |  * syntax tree generated by Esprima (compliant with the Mozilla Parser
   4 |  * API at https://developer.mozilla.org/en-US/docs/SpiderMonkey/Parser_API).
   5 |  *
   6 |  * Dependencies: esprima.js, underscore.js
   7 |  */
   8 | (function(global) {
   9 |     /* Detect npm versus browser usage */
  10 |     var exports;
  11 |     var esprima;
  12 |     var _;
  13 | 
  14 |     // Cache all the structure tests
  15 |     var structureCache = {};
  16 | 
  17 |     // Cache the most recently-parsed code and tree
  18 |     var cachedCode;
  19 |     var cachedCodeTree;
  20 | 
  21 |     if (typeof module !== "undefined" && module.exports) {
  22 |         exports = module.exports = {};
  23 |         esprima = require("./external/esprima.js");
  24 |         _ = require("underscore");
  25 |     } else {
  26 |         exports = this.Structured = {};
  27 |         esprima = global.esprima;
  28 |         _ = global._;
  29 |     }
  30 | 
  31 |     if (!esprima || !_) {
  32 |         throw "Error: Both Esprima and UnderscoreJS are required dependencies.";
  33 |     }
  34 | 
  35 |     /*
  36 |      * Introspects a callback to determine it's parameters and then
  37 |      * produces a constraint that contains the appropriate variables and callbacks.
  38 |      *
  39 |      * This allows a much terser definition of callback function where you don't have to
  40 |      * explicitly state the parameters in a separate list
  41 |      */
  42 |     function makeConstraint(callback) {
  43 |         var paramText = /^function [^\(]*\(([^\)]*)\)/.exec(callback)[1];
  44 |         var params = paramText.match(/[$_a-zA-z0-9]+/g);
  45 | 
  46 |         for (var key in params) {
  47 |             if (params[key][0] !== "$") {
  48 |                 console.warn("Invalid parameter in constraint (should begin with a '$'): ", params[key]);
  49 |                 return null;
  50 |             }
  51 |         }
  52 |         return {
  53 |             variables: params,
  54 |             fn: callback
  55 |         };
  56 |     }
  57 | 
  58 |     /*
  59 |      * return true if n2 < n1 (according to relatively arbitrary criteria)
  60 |      */
  61 |     function shouldSwap(n1, n2) {
  62 | 	if (n1.type < n2.type) { //Sort by node type if different
  63 | 	    return false;
  64 | 	} else if (n1.type > n2.type) {
  65 | 	    return true;
  66 | 	} else if (n1.type === "Literal") { //Sort by value if they're literals
  67 | 	    return n1.raw > n2.raw;
  68 | 	} else { //Otherwise, loop through the properties until a difference is found and sort by that
  69 | 	    for (var k in n1) {
  70 | 		if (n1[k].hasOwnProperty("type") && n1[k] !== n2[k]) {
  71 | 		    return shouldSwap(n1[k], n2[k]);
  72 | 		}
  73 | 	    }
  74 | 	}
  75 |     }
  76 |     function standardizeTree(tree) {
  77 | 	if (!tree) {return tree;}
  78 |         var r = deepClone(tree);
  79 |         switch (tree.type) {
  80 |             case "BinaryExpression":
  81 |                 if (_.contains(["*", "+", "===", "!==", "==", "!=", "&", "|", "^"], tree.operator)) {
  82 | 		    if (shouldSwap(tree.left, tree.right)) {
  83 | 			r.left = standardizeTree(tree.right);
  84 | 			r.right = standardizeTree(tree.left);
  85 |             } else {
  86 |                 r.left = standardizeTree(tree.left);
  87 |                 r.right = standardizeTree(tree.right);
  88 |             }
  89 | 		} else if (tree.operator[0] === ">") {
  90 | 		    r.operator = "<" + tree.operator.slice(1);
  91 | 		    r.left = standardizeTree(tree.right);
  92 | 		    r.right = standardizeTree(tree.left);
  93 | 		} break;
  94 | 	    case "LogicalExpression":
  95 | 	        if (_.contains(["&&", "||"], tree.operator) &&
  96 | 		    shouldSwap(tree.left, tree.right)) {
  97 | 		    r.left = standardizeTree(tree.right);
  98 | 		    r.right = standardizeTree(tree.left);
  99 | 		} break;
 100 | 	    case "AssignmentExpression":
 101 | 	        if (_.contains(["+=", "-=", "*=", "/=", "%=", "<<=", ">>=", ">>>=", "&=", "^=", "|="], tree.operator)) {
 102 | 		    var l = standardizeTree(tree.left);
 103 | 		    r = {type: "AssignmentExpression",
 104 | 			 operator: "=",
 105 | 			 left: l,
 106 | 			 right: {type: "BinaryExpression",
 107 | 				 operator: tree.operator.slice(0,-1),
 108 | 				 left: l,
 109 | 				 right: standardizeTree(tree.right)}};
 110 | 		} else {
 111 |             r.left = standardizeTree(r.left);
 112 |             r.right = standardizeTree(r.right);
 113 |         } break;
 114 | 	    case "UpdateExpression":
 115 | 	        if (_.contains(["++", "--"], tree.operator)) {
 116 | 		    var l = standardizeTree(tree.argument);
 117 | 		    r = {type: "AssignmentExpression",
 118 | 			 operator: "=",
 119 | 			 left: l,
 120 | 			 right: {type: "BinaryExpression",
 121 | 				 operator: tree.operator[0],
 122 | 				 left: l,
 123 | 				 right: {type: "Literal",
 124 | 					 value: 1,
 125 | 					 raw: "1"}}};
 126 | 		} break;
 127 | 	    case "VariableDeclaration":
 128 | 	        if (tree.kind === "var") {
 129 | 		    r = [deepClone(tree)];
 130 | 		    for (var i in tree.declarations) {
 131 | 			if (tree.declarations[i].type === "VariableDeclarator" &&
 132 | 			    tree.declarations[i].init !== null) {
 133 | 			    r.push({type: "ExpressionStatement",
 134 | 				    expression: {type: "AssignmentExpression",
 135 | 						 operator: "=",
 136 | 						 left: tree.declarations[i].id,
 137 | 						 right: standardizeTree(tree.declarations[i].init)}});
 138 | 			    r[0].declarations[i].init = null;
 139 | 			}
 140 | 		    }
 141 | 		} break;
 142 |         case "Literal":
 143 |             r.raw = tree.raw
 144 |                 .replace(/^(?:\"(.*?)\"|\'(.*?)\')$/, function(match, p1, p2) {
 145 |                     return "\"" + ((p1 || "") + (p2 || ""))
 146 |                         .replace(/"|'/g, "\"") + "\"";
 147 |                 });
 148 |             break;
 149 | 	    default:
 150 | 	        for (var key in tree) {
 151 | 		    if (!tree.hasOwnProperty(key) || !_.isObject(tree[key])) {
 152 | 			continue;
 153 | 		    }
 154 | 		    if (_.isArray(tree[key])) {
 155 | 			var ar = [];
 156 | 			for (var i in tree[key]) {  /* jshint forin:false */
 157 | 			    ar = ar.concat(standardizeTree(tree[key][i]));
 158 | 			}
 159 | 			r[key] = ar;
 160 | 		    } else {
 161 | 			r[key] = standardizeTree(tree[key]);
 162 | 		    }
 163 | 		}
 164 |         }
 165 |         return r;
 166 |     }
 167 | 
 168 |     /*
 169 |      * Returns true if the code (a string) matches the structure in rawStructure
 170 |      * Throws an exception if code is not parseable.
 171 |      *
 172 |      * Example:
 173 |      *     var code = "if (y > 30 && x > 13) {x += y;}";
 174 |      *     var rawStructure = function structure() { if (_) {} };
 175 |      *     match(code, rawStructure);
 176 |      *
 177 |      * options.varCallbacks is an object that maps user variable strings like
 178 |      *  "$myVar", "$a, $b, $c" etc to callbacks. These callbacks receive the
 179 |      *  potential Esprima structure values assigned to each of the user
 180 |      *  variables specified in the string, and can accept/reject that value
 181 |      *  by returning true/false. The callbacks can also specify a failure
 182 |      *  message instead by returning an object of the form
 183 |      *  {failure: "Your failure message"}, in which case the message will be
 184 |      *  returned as the property "failure" on the varCallbacks object if
 185 |      *  there is no valid match. A valid matching requires that every
 186 |      *  varCallback return true.
 187 |      *
 188 |      * Advanced Example:
 189 |      *   var varCallbacks = [
 190 |      *     function($foo) {
 191 |      *         return $foo.value > 92;
 192 |      *     },
 193 |      *     function($foo, $bar, $baz) {
 194 |      *         if ($foo.value > $bar.value) {
 195 |      *            return {failure: "Check the relationship between values."};
 196 |      *         }
 197 |      *         return $baz.value !== 48;
 198 |      *     }
 199 |      *   ];
 200 |      *   var code = "var a = 400; var b = 120; var c = 500; var d = 49;";
 201 |      *   var rawStructure = function structure() {
 202 |      *       var _ = $foo; var _ = $bar; var _ = $baz;
 203 |      *   };
 204 |      *   match(code, rawStructure, {varCallbacks: varCallbacks});
 205 |      */
 206 |     var originalVarCallbacks;
 207 |     function match(code, rawStructure, options) {
 208 |         options = options || {};
 209 |         // Many possible inputs formats are accepted for varCallbacks
 210 |         // Constraints can be:
 211 |         // 1. a function (from which we will extract the variables)
 212 |         // 2. an objects (which already has separate .fn and .variables properties)
 213 |         //
 214 |         // It will also accept a list of either of the above (or a mix of the two).
 215 |         // Finally it can accept an object for which the keys are the variables and
 216 |         // the values are the callbacks (This option is mainly for historical reasons)
 217 |         var varCallbacks = options.varCallbacks || [];
 218 |         // We need to keep a hold of the original varCallbacks object because
 219 |         // When structured first came out it returned the failure message by
 220 |         // changing the .failure property on the varCallbacks object and some uses rely on that.
 221 |         // We hope to get rid of this someday.
 222 |         // TODO: Change over the code so to have a better API
 223 |         originalVarCallbacks = varCallbacks;
 224 |         if (varCallbacks instanceof Function || (varCallbacks.fn && varCallbacks.variables)) {
 225 |             varCallbacks = [varCallbacks];
 226 |         }
 227 |         if (varCallbacks instanceof Array) {
 228 |             for (var key in varCallbacks) {
 229 |                 if (varCallbacks[key] instanceof Function) {
 230 |                     varCallbacks[key] = makeConstraint(varCallbacks[key]);
 231 |                 }
 232 |             }
 233 |         } else {
 234 |             var realCallbacks = [];
 235 |             for (var vars in varCallbacks) {
 236 |                 if (varCallbacks.hasOwnProperty(vars) && vars !== "failure") {
 237 |                     realCallbacks.push({
 238 |                         variables: vars.match(/[$_a-zA-z0-9]+/g),
 239 |                         fn: varCallbacks[vars]
 240 |                     });
 241 |                 }
 242 |             }
 243 |             varCallbacks = realCallbacks;
 244 |         }
 245 |         var wildcardVars = {
 246 |             order: [],
 247 |             skipData: {},
 248 |             values: {}
 249 |         };
 250 |         // Note: After the parse, structure contains object references into
 251 |         // wildcardVars[values] that must be maintained. So, beware of
 252 |         // JSON.parse(JSON.stringify), etc. as the tree is no longer static.
 253 |         var structure = parseStructureWithVars(rawStructure, wildcardVars);
 254 | 
 255 |         // Cache the parsed code tree, or pull from cache if it exists
 256 |         var codeTree = (cachedCode === code ?
 257 |             cachedCodeTree :
 258 |             typeof code === "object" ?
 259 |             deepClone(code) :
 260 |             esprima.parse(code));
 261 | 
 262 |         cachedCode = code;
 263 |         cachedCodeTree = codeTree;
 264 | 
 265 |         foldConstants(codeTree);
 266 |         var toFind = structure.body || structure;
 267 |         var peers = [];
 268 |         if (_.isArray(structure.body)) {
 269 |             toFind = structure.body[0];
 270 |             peers = structure.body.slice(1);
 271 |         }
 272 |         var result;
 273 |         var matchResult = {
 274 |             _: [],
 275 |             vars: {}
 276 |         };
 277 | 	codeTree = standardizeTree(codeTree);
 278 |         if (wildcardVars.order.length === 0 || options.single) {
 279 |             // With no vars to match, our normal greedy approach works great.
 280 |             result = checkMatchTree(codeTree, toFind, peers, wildcardVars, matchResult, options);
 281 |         } else {
 282 |             // If there are variables to match, we must do a potentially
 283 |             // exhaustive search across the possible ways to match the vars.
 284 |             result = anyPossible(0, wildcardVars, varCallbacks, matchResult, options);
 285 |         }
 286 |         return result;
 287 | 
 288 |         /*
 289 |          * Checks whether any possible valid variable assignment for this i
 290 |          *  results in a valid match.
 291 |          *
 292 |          * We orchestrate this check by building skipData, which specifies
 293 |          *  for each variable how many possible matches it should skip before
 294 |          *  it guesses a match. The iteration over the tree is the same
 295 |          *  every time -- if the first guess fails, the next run will skip the
 296 |          *  first guess and instead take the second appearance, and so on.
 297 |          *
 298 |          * When there are multiple variables, changing an earlier (smaller i)
 299 |          *  variable guess means that we must redo the guessing for the later
 300 |          *  variables (larger i).
 301 |          *
 302 |          * Returning false involves exhausting all possibilities. In the worst
 303 |          *  case, this will mean exponentially many possibilities -- variables
 304 |          *  are expensive for all but small tests.
 305 |          *
 306 |          * wildcardVars = wVars:
 307 |          *     .values[varName] contains the guessed node value of each
 308 |          *     variable, or the empty object if none.
 309 |          *     .skipData[varName] contains the number of potential matches of
 310 |          *          this var to skip before choosing a guess to assign to values
 311 |          *     .leftToSkip[varName] stores the number of skips left to do
 312 |          *         (used during the match algorithm)
 313 |          *     .order[i] is the name of the ith occurring variable.
 314 |          */
 315 |         function anyPossible(i, wVars, varCallbacks, matchResults, options) {
 316 |             var order = wVars.order; // Just for ease-of-notation.
 317 |             wVars.skipData[order[i]] = 0;
 318 |             do {
 319 |                 // Reset the skip # for all later variables.
 320 |                 for (var rest = i + 1; rest < order.length; rest += 1) {
 321 |                     wVars.skipData[order[rest]] = 0;
 322 |                 }
 323 |                 // Check for a match only if we have reached the last var in
 324 |                 // order (and so set skipData for all vars). Otherwise,
 325 |                 // recurse to check all possible values of the next var.
 326 |                 if (i === order.length - 1) {
 327 |                     // Reset the wildcard vars' guesses. Delete the properties
 328 |                     // rather than setting to {} in order to maintain shared
 329 |                     // object references in the structure tree (toFind, peers)
 330 |                     _.each(wVars.values, function(value, key) {
 331 |                         _.each(wVars.values[key], function(v, k) {
 332 |                             delete wVars.values[key][k];
 333 |                         });
 334 |                     });
 335 |                     wVars.leftToSkip = _.extend({}, wVars.skipData);
 336 |                     // Use a copy of peers because peers is destructively
 337 |                     // modified in checkMatchTree (via checkNodeArray).
 338 |                     if (checkMatchTree(codeTree, toFind, peers.slice(), wVars, matchResults, options) &&
 339 |                         checkUserVarCallbacks(wVars, varCallbacks)) {
 340 |                         return matchResults;
 341 |                     }
 342 |                 } else if (anyPossible(i + 1, wVars, varCallbacks, matchResults, options)) {
 343 |                     return matchResults;
 344 |                 }
 345 |                 // This guess didn't work out -- skip it and try the next.
 346 |                 wVars.skipData[order[i]] += 1;
 347 |                 // The termination condition is when we have run out of values
 348 |                 // to skip and values is no longer defined for this var after
 349 |                 // the match algorithm. That means that there is no valid
 350 |                 // assignment for this and later vars given the assignments to
 351 |                 // previous vars (set by skipData).
 352 |             } while (!_.isEmpty(wVars.values[order[i]]));
 353 |             return false;
 354 |         }
 355 |     }
 356 | 
 357 |     /*
 358 |      * Checks the user-defined variable callbacks and returns a boolean for
 359 |      *   whether or not the wVars assignment of the wildcard variables results
 360 |      *   in every varCallback returning true as required.
 361 |      *
 362 |      * If any varCallback returns false, this function also returns false.
 363 |      *
 364 |      * Format of varCallbacks: An object containing:
 365 |      *     keys of the form: "$someVar" or "$foo, $bar, $baz" to mimic an
 366 |      *        array (as JS keys must be strings).
 367 |      *     values containing function callbacks. These callbacks must return
 368 |      *        true/false. They may alternately return an object of the form
 369 |      *        {failure: "The failure message."}. If the callback returns the
 370 |      *        failure object, then the relevant failure message will be returned
 371 |      *        via varCallbacks.failure.
 372 |      *        These callbacks are passed a parameter list corresponding to
 373 |      *         the Esprima parse structures assigned to the variables in
 374 |      *         the key (see example).
 375 |      *
 376 |      * Example varCallbacks object:
 377 |      *    {
 378 |      *     "$foo": function(fooObj) {
 379 |      *         return fooObj.value > 92;
 380 |      *     },
 381 |      *     "$foo, $bar, $baz": function(fooObj, barObj, bazObj) {
 382 |      *         if (fooObj.value > barObj.value) {
 383 |      *            return {failure: "Check the relationship between values."}
 384 |      *         }
 385 |      *         return bazObj !== 48;
 386 |      *     }
 387 |      *   }
 388 |      */
 389 |     function checkUserVarCallbacks(wVars, varCallbacks) {
 390 |         // Clear old failure message if needed
 391 |         delete originalVarCallbacks.failure;
 392 |         for (var key in varCallbacks) {  /* jshint forin:false */
 393 |             // Property strings may be "$foo, $bar, $baz" to mimic arrays.
 394 |             var varNames = varCallbacks[key].variables;
 395 |             var varValues = _.map(varNames, function(varName) {
 396 |                 varName = stringLeftTrim(varName); // Trim whitespace
 397 |                 // If the var name is in the structure, then it will always
 398 |                 // exist in wVars.values after we find a match prior to
 399 |                 // checking the var callbacks. So, if a variable name is not
 400 |                 // defined here, it is because that var name does not exist in
 401 |                 // the user-defined structure.
 402 |                 if (!_.has(wVars.values, varName)) {
 403 |                     console.error("Callback var " + varName + " doesn't exist");
 404 |                     return undefined;
 405 |                 }
 406 |                 // Convert each var name to the Esprima structure it has
 407 |                 // been assigned in the parse. Make a deep copy.
 408 |                 return deepClone(wVars.values[varName]);
 409 |             });
 410 |             // Call the user-defined callback, passing in the var values as
 411 |             // parameters in the order that the vars were defined in the
 412 |             // property string.
 413 |             var result = varCallbacks[key].fn.apply(null, varValues);
 414 |             if (!result || _.has(result, "failure")) {
 415 |                 // Set the failure message if the user callback provides one.
 416 |                 if (_.has(result, "failure")) {
 417 |                     originalVarCallbacks.failure = result.failure;
 418 |                 }
 419 |                 return false;
 420 |             }
 421 |         }
 422 |         return true;
 423 | 
 424 |         /* Trim is only a string method in IE9+, so use a regex if needed. */
 425 |         function stringLeftTrim(str) {
 426 |             if (String.prototype.trim) {
 427 |                 return str.trim();
 428 |             }
 429 |             return str.replace(/^\s+|\s+$/g, "");
 430 |         }
 431 |     }
 432 | 
 433 |     function parseStructure(structure) {
 434 |         if (typeof structure === "object") {
 435 |             return deepClone(structure);
 436 |         }
 437 | 
 438 |         if (structureCache[structure]) {
 439 |             return JSON.parse(structureCache[structure]);
 440 |         }
 441 | 
 442 |         // Wrapped in parentheses so function() {} becomes valid Javascript.
 443 |         var fullTree = esprima.parse("(" + structure + ")");
 444 | 
 445 |         if (fullTree.body[0].expression.type !== "FunctionExpression" ||
 446 |             !fullTree.body[0].expression.body) {
 447 |             throw "Poorly formatted structure code";
 448 |         }
 449 | 
 450 |         var tree = fullTree.body[0].expression.body;
 451 |         structureCache[structure] = JSON.stringify(tree);
 452 |         return tree;
 453 |     }
 454 | 
 455 |     /*
 456 |      * Returns a tree parsed out of the structure. The returned tree is an
 457 |      *    abstract syntax tree with wildcard properties set to undefined.
 458 |      *
 459 |      * structure is a specification looking something like:
 460 |      *        function structure() {if (_) { var _ = 3; }}
 461 |      *    where _ denotes a blank (anything can go there),
 462 |      *    and code can go before or after any statement (only the nesting and
 463 |      *        relative ordering matter).
 464 |      */
 465 |     function parseStructureWithVars(structure, wVars) {
 466 |         var tree = standardizeTree(parseStructure(structure));
 467 |         foldConstants(tree);
 468 |         simplifyTree(tree, wVars);
 469 |         return tree;
 470 |     }
 471 | 
 472 |     /*
 473 |      * Constant folds the syntax tree
 474 |      */
 475 |     function foldConstants(tree) {
 476 |         for (var key in tree) {  /* jshint forin:false */
 477 |             if (!tree.hasOwnProperty(key)) {
 478 |                 continue; // Inherited property
 479 |             }
 480 | 
 481 |             var ast = tree[key];
 482 |             if (_.isObject(ast)) {
 483 |                 foldConstants(ast);
 484 | 
 485 |                 /*
 486 |                  * Currently, we only fold + and - applied to a number literal.
 487 |                  * This is easy to extend, but it means we lose the ability to match
 488 |                  * potentially useful expressions like 5 + 5 with a pattern like _ + _.
 489 |                  */
 490 |                 /* jshint eqeqeq:false */
 491 |                 if (ast.type == esprima.Syntax.UnaryExpression) {
 492 |                     var argument = ast.argument;
 493 |                     if (argument.type === esprima.Syntax.Literal &&
 494 |                         _.isNumber(argument.value)) {
 495 |                         if (ast.operator === "-") {
 496 |                             argument.value = -argument.value;
 497 |                             tree[key] = argument;
 498 |                         } else if (ast.operator === "+") {
 499 |                             argument.value = +argument.value;
 500 |                             tree[key] = argument;
 501 |                         }
 502 |                     }
 503 |                 }
 504 |             }
 505 |         }
 506 |     }
 507 | 
 508 |     /*
 509 |      * Recursively traverses the tree and sets _ properties to undefined
 510 |      * and empty bodies to null.
 511 |      *
 512 |      *  Wildcards are explicitly set to undefined -- these undefined properties
 513 |      *  must exist and be non-null in order for code to match the structure.
 514 |      *
 515 |      *  Wildcard variables are set up such that the first occurrence of the
 516 |      *   variable in the structure tree is set to {wildcardVar: varName},
 517 |      *   and all later occurrences just refer to wVars.values[varName],
 518 |      *   which is an object assigned during the matching algorithm to have
 519 |      *   properties identical to our guess for the node matching the variable.
 520 |      *   (maintaining the reference). In effect, these later accesses
 521 |      *   to tree[key] mimic tree[key] simply being set to the variable value.
 522 |      *
 523 |      *  Empty statements are deleted from the tree -- they need not be matched.
 524 |      *
 525 |      *  If the subtree is an array, we just iterate over the array using
 526 |      *    for (var key in tree)
 527 |      *
 528 |      */
 529 |     function simplifyTree(tree, wVars) {
 530 |         for (var key in tree) {  /* jshint forin:false */
 531 |             if (!tree.hasOwnProperty(key)) {
 532 |                 continue; // Inherited property
 533 |             }
 534 |             if (_.isObject(tree[key])) {
 535 |                 if (isWildcard(tree[key])) {
 536 |                     tree[key] = undefined;
 537 |                 } else if (isWildcardVar(tree[key])) {
 538 |                     var varName = tree[key].name;
 539 |                     if (!wVars.values[varName]) {
 540 |                         // Perform setup for the first occurrence.
 541 |                         wVars.values[varName] = {}; // Filled in later.
 542 |                         tree[key] = {
 543 |                             wildcardVar: varName
 544 |                         };
 545 |                         wVars.order.push(varName);
 546 |                         wVars.skipData[varName] = 0;
 547 |                     } else {
 548 |                         tree[key] = wVars.values[varName]; // Reference.
 549 |                     }
 550 |                 } else if (tree[key].type === esprima.Syntax.EmptyStatement) {
 551 |                     // Arrays are objects, but delete tree[key] does not
 552 |                     //  update the array length property -- so, use splice.
 553 |                     _.isArray(tree) ? tree.splice(key, 1) : delete tree[key];
 554 |                 } else {
 555 |                     simplifyTree(tree[key], wVars);
 556 |                 }
 557 |             }
 558 |         }
 559 |     }
 560 | 
 561 |     /*
 562 |      * Returns whether the structure node is intended as a wildcard node, which
 563 |      * can be filled in by anything in others' code.
 564 |      */
 565 |     function isWildcard(node) {
 566 |         return node.name && node.name === "_";
 567 |     }
 568 | 
 569 |     /* Returns whether the structure node is intended as a wildcard variable. */
 570 |     function isWildcardVar(node) {
 571 |         return (node.name && _.isString(node.name) && node.name.length >= 2 &&
 572 |             node.name[0] === "$");
 573 |     }
 574 | 
 575 |     /*
 576 |      *
 577 |      */
 578 |     function isGlob(node) {
 579 |         return node && node.name &&
 580 |             ((node.name === "glob_" && "_") ||
 581 |                 (node.name.indexOf("glob$") === 0 && node.name.slice(5))) ||
 582 |             node && node.expression && isGlob(node.expression);
 583 |     }
 584 | 
 585 |     /*
 586 |      * Returns true if currTree matches the wildcard structure toFind.
 587 |      *
 588 |      * currTree: The syntax node tracking our current place in the user's code.
 589 |      * toFind: The syntax node from the structure that we wish to find.
 590 |      * peersToFind: The remaining ordered syntax nodes that we must find after
 591 |      *     toFind (and on the same level as toFind).
 592 |      * modify: should it call RestructureTree()?
 593 |      */
 594 |     function checkMatchTree(currTree, toFind, peersToFind, wVars, matchResults, options) {
 595 |         if (_.isArray(toFind)) {
 596 |             console.error("toFind should never be an array.");
 597 |             console.error(toFind);
 598 |         }
 599 |         /* jshint -W041, -W116 */
 600 |         if (currTree == undefined) {
 601 |             if (toFind == undefined) {
 602 |                 matchResults._.push(currTree);
 603 |                 return matchResults;
 604 |             } else {
 605 |                 return false;
 606 |             }
 607 |         }
 608 |         if (exactMatchNode(currTree, toFind, peersToFind, wVars, matchResults, options)) {
 609 |             return matchResults;
 610 |         }
 611 |         // Don't recurse if we're just checking a single node.
 612 |         if (options.single) {
 613 |             return false;
 614 |         }
 615 |         // Check children.
 616 |         for (var key in currTree) {  /* jshint forin:false */
 617 |             if (!currTree.hasOwnProperty(key) || !_.isObject(currTree[key])) {
 618 |                 continue; // Skip inherited properties
 619 |             }
 620 |             // Recursively check for matches
 621 |             if ((_.isArray(currTree[key]) &&
 622 |                     checkNodeArray(currTree[key], toFind, peersToFind, wVars, matchResults, options, true)) ||
 623 |                 (!_.isArray(currTree[key]) &&
 624 |                     checkMatchTree(currTree[key], toFind, peersToFind, wVars, matchResults, options, true))) {
 625 |                 return matchResults;
 626 |             }
 627 |         }
 628 |         return false;
 629 |     }
 630 | 
 631 |     /*
 632 |      * Returns true if this level of nodeArr matches the node in
 633 |      * toFind, and also matches all the nodes in peersToFind in order.
 634 |      */
 635 |     function checkNodeArray(nodeArr, toFind, peersToFind, wVars, matchResults, options) {
 636 |         var curGlob;
 637 | 
 638 |         for (var i = 0; i < nodeArr.length; i += 1) {
 639 |             if (isGlob(toFind)) {
 640 |                 if (!curGlob) {
 641 |                     curGlob = [];
 642 |                     var globName = isGlob(toFind);
 643 |                     if (globName === "_") {
 644 |                         matchResults._.push(curGlob);
 645 |                     } else {
 646 |                         matchResults.vars[globName] = curGlob;
 647 |                     }
 648 |                 }
 649 |                 curGlob.push(nodeArr[i]);
 650 |             } else if (checkMatchTree(nodeArr[i], toFind, peersToFind, wVars, matchResults, options)) {
 651 |                 if (!peersToFind || peersToFind.length === 0) {
 652 |                     return matchResults;
 653 |                     // Found everything needed on this level.
 654 |                 } else {
 655 |                     // We matched this node, but we still have more nodes on
 656 |                     // this level we need to match on subsequent iterations
 657 |                     toFind = peersToFind.shift(); // Destructive.
 658 |                 }
 659 |             }
 660 |         }
 661 | 
 662 |         if (curGlob) {
 663 |             return matchResults;
 664 |         } else if (isGlob(toFind)) {
 665 |             var globName = isGlob(toFind);
 666 |             if (globName === "_") {
 667 |                 matchResults._.push([]);
 668 |             } else {
 669 |                 matchResults.vars[globName] = [];
 670 |             }
 671 |             return matchResults;
 672 |         }
 673 | 
 674 |         return false;
 675 |     }
 676 | 
 677 | 
 678 |     /*
 679 |      * This discards all wildcard vars that were part of a failed match
 680 |      * this provides an important speedup by stopping anyPossible from having to increment
 681 |      * every match on a doomed set of arguments.
 682 |      * If the any argument set fails no amount of incrementing can save it.
 683 |      */
 684 |     function discardWVarsOnFailureDecorator(callback) {
 685 |         return function(currTree, toFind, peersToFind, wVars, matchResults, options) {
 686 |             var lastWVar;
 687 |             for (lastWVar=0; lastWVar 0) {
 803 |                         wVars.leftToSkip[subFind] -= 1;
 804 |                         return false; // Skip, this does not match our wildcard
 805 |                     }
 806 |                     // We have skipped the required number, so take this guess.
 807 |                     // Copy over all of currNode's properties into
 808 |                     //  wVars.values[subFind] so the var references set up in
 809 |                     //  simplifyTree behave like currNode. Shallow copy.
 810 |                     _.extend(wVars.values[subFind], currNode);
 811 |                     matchResults.vars[subFind.slice(1)] = currNode;
 812 |                     if (rootToSet) {
 813 |                         matchResults.root = rootToSet;
 814 |                     }
 815 |                     return matchResults; // This node is now our variable.
 816 |                 }
 817 |                 return false;
 818 |             }
 819 |             // Now handle arrays/objects/values
 820 |             if (_.isObject(subCurr) !== _.isObject(subFind) ||
 821 |                 _.isArray(subCurr) !== _.isArray(subFind) ||
 822 |                 (typeof(subCurr) !== typeof(subFind))) {
 823 |                 return false;
 824 |             } else if (_.isArray(subCurr)) {
 825 |                 // Both are arrays, do a recursive compare.
 826 |                 // (Arrays are objects so do this check before the object check)
 827 |                 if (subFind.length === 0) {
 828 |                     continue; // Empty arrays can match any array.
 829 |                 }
 830 |                 var newToFind = subFind[0];
 831 |                 var peers = subFind.slice(1);
 832 |                 if (key === "params" || key === "arguments") {
 833 |                     if (!checkArgumentsArray(subCurr, newToFind, peers, wVars, matchResults, options)) {
 834 |                         return false;
 835 |                     }
 836 |                 } else if (!checkNodeArray(subCurr, newToFind, peers, wVars, matchResults, options)) {
 837 |                     return false;
 838 |                 }
 839 |             } else if (_.isObject(subCurr)) {
 840 |                 // Both are objects, so do a recursive compare.
 841 |                 if (!checkMatchTree(subCurr, subFind, peersToFind, wVars, matchResults, options)) {
 842 |                     return false;
 843 |                 }
 844 |             } else {
 845 |                 // Check that the non-object (number/string) values match
 846 |                 if (subCurr !== subFind) {
 847 |                     return false;
 848 |                 }
 849 |             }
 850 |         }
 851 |         if (toFind === undefined) {
 852 |             matchResults._.push(currNode);
 853 |         }
 854 |         if (rootToSet) {
 855 |             matchResults.root = rootToSet;
 856 |         }
 857 |         return matchResults;
 858 |     }
 859 | 
 860 |     function deepClone(obj) {
 861 |         return JSON.parse(JSON.stringify(obj));
 862 |     }
 863 | 
 864 |     /*
 865 |      * Takes in a string for a structure and returns HTML for nice styling.
 866 |      * The blanks (_) are enclosed in span.structuredjs_blank, and the
 867 |      * structured.js variables ($someVar) are enclosed in span.structuredjs_var
 868 |      * for special styling.
 869 |      *
 870 |      * See pretty-display/index.html for a demo and sample stylesheet.
 871 |      *
 872 |      * Only works when RainbowJS (http://craig.is/making/rainbows) is
 873 |      * included on the page; if RainbowJS is not available, simply
 874 |      * returns the code string. RainbowJS is not available as an npm
 875 |      * module.
 876 |      */
 877 |     function prettyHtml(code, callback) {
 878 |         if (!Rainbow) {
 879 |             return code;
 880 |         }
 881 |         Rainbow.color(code, "javascript", function(formattedCode) {
 882 |             var output = ("
" +
 883 |                 addStyling(formattedCode) + "
"); 884 | callback(output); 885 | }); 886 | } 887 | 888 | /* 889 | * Helper function for prettyHtml that takes in a string (the formatted 890 | * output of RainbowJS) and inserts special StructuredJS spans for 891 | * blanks (_) and variables ($something). 892 | * 893 | * The optional parameter maintainStyles should be set to true if the 894 | * caller wishes to keep the class assignments from the previous call 895 | * to addStyling and continue where we left off. This parameter is 896 | * valuable for visual consistency across different structures that share 897 | * variables. 898 | */ 899 | function addStyling(code, maintainStyles) { 900 | if (!maintainStyles) { 901 | addStyling.styleMap = {}; 902 | addStyling.counter = 0; 903 | } 904 | // First replace underscores with empty structuredjs_blank spans 905 | // Regex: Match any underscore _ that is not preceded or followed by an 906 | // alphanumeric character. 907 | code = code.replace(/(^|[^A-Za-z0-9])_(?![A-Za-z0-9])/g, 908 | "$1"); 909 | // Next replace variables with empty structuredjs_var spans numbered 910 | // with classes. 911 | // This regex is in two parts: 912 | // Part 1, delimited by the non-capturing parentheses `(?: ...)`: 913 | // (^|[^\w])\$(\w+) 914 | // Match any $ that is preceded by either a 'start of line', or a 915 | // non-alphanumeric character, and is followed by at least one 916 | // alphanumeric character (the variable name). 917 | // Part 2, also delimited by the non-capturing parentheses: 918 | // ()\$(\w+)<\/span> 919 | // Match any function call immediately preceded by a dollar sign, 920 | // where the Rainbow syntax highlighting separated a $foo() 921 | // function call by placing the dollar sign outside. 922 | // the function call span to create 923 | // $foo. 924 | // We combine the two parts with an | (an OR) so that either matches. 925 | // The reason we do this all in one go rather than in two separate 926 | // calls to replace is so that we color the string in order, 927 | // rather than coloring all non-function calls and then going back 928 | // to do all function calls (a minor point, but otherwise the 929 | // interactive pretty display becomes jarring as previous 930 | // function call colors change when new variables are introduced.) 931 | // Finally, add the /g flag for global replacement. 932 | var regexVariables = /(?:(^|[^\w])\$(\w+))|(?:\$(\w+)<\/span>)/g; 933 | return code.replace(regexVariables, 934 | function(m, prev, varName, fnVarName) { 935 | // Necessary to handle the fact we are essentially performing 936 | // two regexes at once as outlined above. 937 | prev = prev || ""; 938 | varName = varName || fnVarName; 939 | var fn = addStyling; 940 | // Assign the next available class to this variable if it does 941 | // not yet exist in our style mapping. 942 | if (!(varName in fn.styleMap)) { 943 | fn.styleMap[varName] = (fn.counter < fn.styles.length ? 944 | fn.styles[fn.counter] : "extra"); 945 | fn.counter += 1; 946 | } 947 | return (prev + "" + ""); 949 | } 950 | ); 951 | } 952 | // Store some properties on the addStyling function to maintain the 953 | // styleMap between runs if desired. 954 | // Right now just support 7 different variables. Just add more if needed. 955 | addStyling.styles = ["one", "two", "three", "four", "five", "six", 956 | "seven" 957 | ]; 958 | addStyling.styleMap = {}; 959 | addStyling.counter = 0; 960 | 961 | function getSingleData(node, data) { 962 | if (!node || node.type !== "Identifier") { 963 | return; 964 | } 965 | 966 | if (node.name === "_") { 967 | if (!data._ || data._.length === 0) { 968 | throw "No _ data available."; 969 | } 970 | 971 | return data._.shift(); 972 | } else if (node.name && node.name.indexOf("$") === 0) { 973 | var name = node.name.slice(1); 974 | 975 | if (!data.vars || !(name in data.vars)) { 976 | throw "No vars available."; 977 | } 978 | 979 | return data.vars[name]; 980 | } 981 | } 982 | 983 | function getGlobData(node, data) { 984 | var check = node && node.expression || node; 985 | 986 | if (!check || check.type !== "Identifier") { 987 | return; 988 | } 989 | 990 | if (check.name === "glob_") { 991 | if (!data._ || data._.length === 0) { 992 | throw "No _ data available."; 993 | } 994 | 995 | return data._.shift(); 996 | } else if (check.name && check.name.indexOf("glob$") === 0) { 997 | var name = check.name.slice(5); 998 | 999 | if (!data.vars || !(name in data.vars)) { 1000 | throw "No vars available."; 1001 | } 1002 | 1003 | return data.vars[name]; 1004 | } 1005 | } 1006 | 1007 | function injectData(node, data) { 1008 | if (!node) { 1009 | return node; 1010 | } 1011 | 1012 | for (var prop in node) { /* jshint forin:false */ 1013 | if (!node.hasOwnProperty(prop)) { 1014 | continue; 1015 | } 1016 | 1017 | if (node[prop] && typeof node[prop] === "object" && "length" in node[prop]) { 1018 | for (var i = 0; i < node[prop].length; i++) { 1019 | var globData = getGlobData(node[prop][i], data); 1020 | 1021 | if (globData) { 1022 | node[prop].splice.apply(node[prop], 1023 | [i, 1].concat(globData)); 1024 | break; 1025 | } else if (typeof node[prop][i] === "object") { 1026 | var singleData = getSingleData(node[prop][i], data); 1027 | 1028 | if (singleData) { 1029 | node[prop][i] = singleData; 1030 | } else if (typeof node[prop][i] === "object") { 1031 | injectData(node[prop][i], data); 1032 | } 1033 | } 1034 | } 1035 | } else { 1036 | var singleData = getSingleData(node[prop], data); 1037 | 1038 | if (singleData) { 1039 | node[prop] = singleData; 1040 | } else if (typeof node[prop] === "object") { 1041 | injectData(node[prop], data); 1042 | } 1043 | } 1044 | } 1045 | 1046 | return node; 1047 | } 1048 | 1049 | exports.match = match; 1050 | exports.matchNode = function(code, rawStructure, options) { 1051 | options = options || {}; 1052 | options.single = true; 1053 | return match(code, rawStructure, options); 1054 | }; 1055 | exports.injectData = function(node, data) { 1056 | node = parseStructure(node); 1057 | data = deepClone(data); 1058 | return injectData(node, data); 1059 | }; 1060 | exports.prettify = prettyHtml; 1061 | })(typeof window !== "undefined" ? window : global); 1062 | -------------------------------------------------------------------------------- /testrunner.js: -------------------------------------------------------------------------------- 1 | /* Runs the QUnit tests for StructuredJS. `node testrunner.js` */ 2 | 3 | var runner = require("qunit"); 4 | runner.run({ 5 | code: {path: "./structured.js", namespace: "structured"}, 6 | tests: "tests.js" 7 | }); 8 | -------------------------------------------------------------------------------- /tests.js: -------------------------------------------------------------------------------- 1 | /* QUnit tests for StructuredJS. */ 2 | 3 | // Detect if run in-browser or via node. 4 | if (typeof global !== "undefined") { 5 | Structured = global.structured; 6 | } else { 7 | Structured = this.Structured; 8 | } 9 | 10 | var basicTests = function() { 11 | 12 | QUnit.module("Basic detection"); 13 | 14 | test("Accepts string for structure", function() { 15 | ok(Structured.match("var draw = function() {}", 16 | "function() { var draw = function() {};}"), 17 | "Accepts string and matches"); 18 | 19 | equal(Structured.match("var drow = function() {}", 20 | "function() { var draw = function() {};}"), 21 | false, 22 | "Accepts string and doesn't match it"); 23 | }); 24 | 25 | test("Positive tests of syntax", function() { 26 | 27 | ok(Structured.match("", 28 | function() {}), 29 | "Empty structure matches empty string."); 30 | 31 | ok(Structured.match("if (y > 30 && x > 13) {x += y;}", 32 | function() { 33 | if (_) {} 34 | }), 35 | "Basic if-statement structure matches."); 36 | 37 | ok(Structured.match("if (y > 30 && x > 13) {x += y;}", 38 | function foo() { 39 | if (_) {} 40 | }), 41 | "Using a named function is allowable as well."); 42 | 43 | ok(Structured.match("if (y > 30 && x > 13) {x += y;} else { y += 2;}", 44 | function() { 45 | if (_) {} else {} 46 | }), 47 | "Basic if-else statement structure matches."); 48 | 49 | ok(Structured.match("if (y > 30 && x > 13) {x += y;} \ 50 | else if(x <10) {y -= 20;} else { y += 2;}", 51 | function() { 52 | if (_) {} else if (_) {} else {} 53 | }), 54 | "Basic if, else-if, else statement structure matches."); 55 | 56 | ok(Structured.match("for (var a = 0; a < 10; a += 1) { a -= 2;}", 57 | function() { 58 | for (_; _; _) {} 59 | }), 60 | "Basic for-statement structure matches."); 61 | 62 | ok(Structured.match("var a = 30;", 63 | function() { 64 | var _ = _; 65 | }), 66 | "Basic variable declaration + initialization matches."); 67 | 68 | ok(Structured.match("var test = function() {return 3+2;}", 69 | function() { 70 | var _ = function() {}; 71 | }), 72 | "Basic function assignment into var matches."); 73 | 74 | ok(Structured.match("function foo() {return x+2;}", 75 | function() { 76 | function _() {} 77 | }), 78 | "Basic standalone function declaration matches."); 79 | 80 | ok(Structured.match("rect();", 81 | function() { 82 | rect(); 83 | }), 84 | "No-parameter call to function matches."); 85 | 86 | ok(Structured.match("rect(3);", 87 | function() { 88 | rect(); 89 | }), 90 | "Parameterized call to no-param function structure matches."); 91 | 92 | ok(Structured.match("rect(30, 40, 10, 11);", 93 | function() { 94 | rect(30, 40, 10, 11); 95 | }), 96 | "Fully specified parameter call to function matches."); 97 | 98 | ok(Structured.match("rect(30, 40, 10, 11);", 99 | function() { 100 | rect(30, _, _, 11); 101 | }), 102 | "Parameters with wildcards call to function matches."); 103 | 104 | ok(Structured.match("rect(30, 40);", 105 | function() { 106 | rect(_, _); 107 | }), 108 | "Parameters with all wildcards call to function matches."); 109 | 110 | ok(Structured.match("rect(30, 40, 30);", 111 | function() { 112 | rect(_, _); 113 | }), 114 | "Extra params to function matches."); 115 | 116 | ok(Structured.match("[_]", 117 | function() { 118 | [,]; 119 | }), 120 | "Wildcard array matches [,]"); 121 | 122 | ok(Structured.match("[,]", 123 | function() { 124 | []; 125 | }), 126 | "Empty array matches [,]"); 127 | 128 | ok(Structured.match("var myVar = \"abc I love strings!\"", function() { 129 | var _ = 'abc I love strings!'; 130 | }), "\"text\" matches 'text'"); 131 | 132 | ok(Structured.match("var myVar = 'abc I love strings!'", function() { 133 | var _ = "abc I love strings!"; 134 | }), "'text' matches \"text\""); 135 | 136 | ok(Structured.match("var empty1 = '';var empty2 = \"\"", function() { 137 | var _ = ''; 138 | var _ = ""; 139 | }), "Quote style of empty string is ignored"); 140 | }); 141 | 142 | test("Negative tests of syntax", function() { 143 | 144 | equal(Structured.match("if (y > 30 && x > 13) {x += y;}", 145 | function() { 146 | if (_) { 147 | _; 148 | } else {} 149 | }), 150 | false, 151 | "If-else does not match only an if."); 152 | 153 | equal(Structured.match("if(y > 30 && x > 13) {x += y;} else {y += 2;}", 154 | function() { 155 | if (_) {} else if (_) {} else {} 156 | }), 157 | false, 158 | "If, else if, else structure does not match only if else."); 159 | 160 | equal(Structured.match("var a;", 161 | function() { 162 | var _ = _; 163 | }), 164 | false, 165 | "Variable declaration + init does not match just declaration."); 166 | 167 | equal(Structured.match("while(true) { a -= 2;}", 168 | function() { 169 | for (_; _; _) {} 170 | }), 171 | false, 172 | "For-statement does not match a while loop."); 173 | 174 | equal(Structured.match("var test = 3+5", 175 | function() { 176 | var _ = function() {}; 177 | }), 178 | false, 179 | "Basic function declaration does not match basic var declaration"); 180 | 181 | equal(Structured.match("var test = function foo() {return 3+2;}", 182 | function() { 183 | function _() {} 184 | }), 185 | false, 186 | "Function declaration doesn't match function assignment into var"); 187 | 188 | equal(Structured.match("rect();", 189 | function() { 190 | ellipse(); 191 | }), 192 | false, 193 | "Call to function does not match differently-named function."); 194 | 195 | equal(Structured.match("rect(300, 400, 100, 110);", 196 | function() { 197 | rect(30, 40, 10, 11); 198 | }), 199 | false, 200 | "Fully specified parameter call to function identifies mismatch."); 201 | 202 | equal(Structured.match("rect(30);", 203 | function() { 204 | rect(30, 40); 205 | }), 206 | false, 207 | "Too few parameters does not match."); 208 | 209 | equal(Structured.match("rect(60, 40, 10, 11);", 210 | function() { 211 | rect(30, _, _, 11); 212 | }), 213 | false, 214 | "Parameters with wildcards call to function identifies mismatch."); 215 | 216 | equal(Structured.match("rect();", 217 | function() { 218 | rect(_, _); 219 | }), 220 | false, 221 | "Wildcard params do not match no-params for function call."); 222 | 223 | equal(Structured.match("rect(30, 40);", 224 | function() { 225 | rect(_, _, _); 226 | }), 227 | false, 228 | "Parameters with too few wildcards call to function mismatches."); 229 | 230 | equal(Structured.match("[,]", 231 | function() { 232 | [4]; 233 | }), 234 | false, 235 | "[,] doesn't match regular array"); 236 | }); 237 | }; 238 | 239 | var clutterTests = function() { 240 | QUnit.module("Clutter testing"); 241 | test("Simple tests of distracting syntax", function() { 242 | var structure, code; 243 | 244 | ok(Structured.match("if (y > 30 && x > 13) {x += y;} \ 245 | else if (x <10) {y -= 20;} else { y += 2;}", 246 | function() { 247 | if (_) {} else {} 248 | }), 249 | "Extra else-if statement correctly allowed though not specified."); 250 | 251 | structure = function() { 252 | for (; _ < 10; _ += 1) { 253 | if (_) { 254 | 255 | } 256 | } 257 | }; 258 | code = " \ 259 | var x = 30; \n \ 260 | for (var i = 0; i < 10; j += 1) { \n \ 261 | if (y > 30 && x > 13) {x += y;} \n \ 262 | console.log(x); \n \ 263 | }"; 264 | ok(Structured.match(code, structure), 265 | "Correctly matching for-loop condition."); 266 | 267 | code = " \ 268 | var x = 30; \n \ 269 | for (var i = 0; i < 20; i += 1) { \n \ 270 | if (y > 30 && x > 13) {x += y;} \n \ 271 | console.log(x); \n \ 272 | }"; 273 | equal(Structured.match(code, structure), 274 | false, "Correctly not matching for-loop condition."); 275 | }); 276 | }; 277 | 278 | 279 | var nestedTests = function() { 280 | QUnit.module("Nested testing"); 281 | test("More involved tests of nested syntax", function() { 282 | var structure, code; 283 | 284 | structure = function() { 285 | if (_) { 286 | if (_) { 287 | 288 | } 289 | } 290 | }; 291 | code = " \ 292 | var x = 30; \n \ 293 | if (x > 10) { \n \ 294 | if (y > 30 && x > 13) {x += y;} \n \ 295 | console.log(x); \n \ 296 | } \n "; 297 | ok(Structured.match(code, structure), 298 | "Nested if with distractions matches."); 299 | 300 | code = " \ 301 | var x = 30; \n \ 302 | if (x > 10) { \n \ 303 | for (var a = 0; a < 2; a += 1) { \n \ 304 | if (y > 30 && x > 13) {x += y;} \n } \ 305 | console.log(x); \n \ 306 | } \n "; 307 | ok(Structured.match(code, structure), 308 | "More complex nested if with distraction matches."); 309 | 310 | code = " \ 311 | var x = 30; \n \ 312 | if (x > 10) { \n \ 313 | while (y > 30 && x > 13) {x += y;} \n \ 314 | console.log(x); \n \ 315 | } if (y > 30 && x > 13) {x += y;} \n "; 316 | equal(Structured.match(code, structure), 317 | false, "Non-nested if does not match."); 318 | }); 319 | }; 320 | 321 | var drawingTests = function() { 322 | QUnit.module("Draw-specific testing"); 323 | test("Draw loop specific tests", function() { 324 | var structure, code; 325 | 326 | structure = function() { 327 | var draw = function() { 328 | rect(_, _, _, _); 329 | }; 330 | }; 331 | code = " \ 332 | var draw = function() { \n \ 333 | rect(x, y, 30, 30); \n \ 334 | }; \n "; 335 | ok(Structured.match(code, structure), 336 | "Draw with wildcard rect call matches."); 337 | 338 | structure = function() { 339 | var draw = function() { 340 | rect(); 341 | }; 342 | }; 343 | code = " \ 344 | var draw = function() { \n \ 345 | 20; \n \ 346 | rect(); \n \ 347 | }; \n "; 348 | ok(Structured.match(code, structure), 349 | "Draw with wildcard rect call and distractions matches."); 350 | 351 | structure = function() { 352 | var draw = function() { 353 | var _ = 10; 354 | if (_) { 355 | var _ = _; 356 | if (_) { 357 | _ = _ / 2; 358 | _ = _ % 2; 359 | rect(); 360 | } 361 | } 362 | rect(); 363 | ellipse(); 364 | }; 365 | }; 366 | code = " \ 367 | var draw = function() { \n \ 368 | var a = 20; \n \ 369 | var b = 40; \n \ 370 | var c = 10; \n \ 371 | if (a > 10 && b > 30) { \n \ 372 | a -= 3; \n \ 373 | var d = 2; \n \ 374 | if (d > 1 && c > 3) { \n \ 375 | d = 2; \n \ 376 | c = a / 2; \n \ 377 | b = 20; \n \ 378 | d = b; \n \ 379 | a = c % 2; \n \ 380 | rect(); \n \ 381 | ellipse(); \n \ 382 | } d += 10; c *= d; rect(3, c, d, a); \n \ 383 | ellipse(a, a, c, d); rect(); f += 3; \n \ 384 | } rect(); \n \ 385 | var b = 10; \n \ 386 | ellipse(); \n \ 387 | }; \n "; 388 | ok(Structured.match(code, structure), 389 | "Larger drawing test successful."); 390 | }); 391 | }; 392 | 393 | var peerTests = function() { 394 | QUnit.module("Peer detection testing"); 395 | test("Basic detection of neighbors", function() { 396 | var structure, code; 397 | 398 | ok(Structured.match("rect(); ellipse();", 399 | function() { 400 | rect(); 401 | ellipse() 402 | }), 403 | "Back-to-back function calls match."); 404 | 405 | equal(Structured.match("rect();", 406 | function() { 407 | rect(); 408 | ellipse() 409 | }), 410 | false, 411 | "Back-to-back function calls do not match only the first."); 412 | 413 | structure = function() { 414 | var _ = 0; 415 | var _ = 1; 416 | var _ = _; 417 | }; 418 | code = "var a = 0; var b = 1; var c = 30;"; 419 | ok(Structured.match(code, structure), "Back-to-back vars matched."); 420 | equal(Structured.match("var a = 0; var b = 1;", 421 | structure), false, "Partial match does not suffice."); 422 | 423 | structure = function() { 424 | var draw = function() { 425 | rect(); 426 | ellipse(); 427 | var _ = 3; 428 | }; 429 | }; 430 | code = " \ 431 | var draw = function() { \n \ 432 | var a = 20; \n \ 433 | rect(); \n \ 434 | var b = 10; \n \ 435 | ellipse(); \n \ 436 | var c = 30; \n \ 437 | var d = 3; \n \ 438 | c = a + b; \n \ 439 | }; \n "; 440 | ok(Structured.match(code, structure), 441 | "Multi-function call separated by statements matches."); 442 | }); 443 | }; 444 | 445 | var combinedTests = function() { 446 | QUnit.module("Combined functionality tests"); 447 | test("Simple combined tests", function() { 448 | var structure, code; 449 | structure = function() { 450 | if (_ % 2) { 451 | _ += _; 452 | } 453 | }; 454 | code = " \n \ 455 | if (y % 2 == 1 ) { \n \ 456 | x += y; \n \ 457 | } \n \ 458 | "; 459 | equal(!!Structured.match(code, structure), 460 | true, "Simple mod, if, and += works"); 461 | }); 462 | }; 463 | 464 | var varCallbackTests = function() { 465 | QUnit.module("User-defined variable callbacks"); 466 | test("Basic single variable callbacks", function() { 467 | var structure, code; 468 | 469 | equal(Structured.match("var x = 10; var y = 20;", 470 | function() { 471 | var _ = $a; 472 | }, { 473 | "varCallbacks": { 474 | "$a": function(obj) { 475 | return false; 476 | } 477 | } 478 | }), 479 | false, "Always false varCallback causes failure."); 480 | 481 | equal(Structured.match("var x = 10; var y = 20; var k = 42;", 482 | function() { 483 | var _ = $a; 484 | var _ = $b; 485 | }, { 486 | "varCallbacks": { 487 | "$a": function(obj) { 488 | return false; 489 | }, 490 | "$b": function(obj) { 491 | return true; 492 | } 493 | } 494 | }), 495 | false, "One always false varCallback of two causes failure."); 496 | 497 | equal(!!Structured.match("var x = 10; var y = 20;", 498 | function() { 499 | var _ = $a; 500 | }, { 501 | "varCallbacks": { 502 | "$a": function(obj) { 503 | return true; 504 | } 505 | } 506 | }), 507 | true, "Always true varCallback still matches."); 508 | 509 | equal(Structured.match("var x = 10; var y = 20;", 510 | function() { 511 | var z = $a; 512 | }, { 513 | "varCallbacks": { 514 | "$a": function(obj) { 515 | return true; 516 | } 517 | } 518 | }), 519 | false, "Always true varCallback with no match does not match."); 520 | 521 | equal(!!Structured.match("var x = 10; var y = 20; var z = 40;", 522 | function() { 523 | var _ = $a; 524 | }, { 525 | "varCallbacks": { 526 | "$a": function(obj) { 527 | return obj.type === "Literal" && obj.value === 40; 528 | } 529 | } 530 | }), 531 | true, "Basic single-matching var callback matches."); 532 | 533 | equal(!!Structured.match("var x = 10; var y = 20; var z = 40;", 534 | function() { 535 | var _ = $a; 536 | var _ = $b; 537 | }, { 538 | "varCallbacks": { 539 | "$a": function(obj) { 540 | return obj.type === "Literal" && obj.value > 11; 541 | }, 542 | "$b": function(obj) { 543 | return obj.type === "Literal" && obj.value > 30; 544 | } 545 | } 546 | }), 547 | true, "Two single-matching var callbacks match correctly."); 548 | 549 | equal(Structured.match("var x = 10; var y = 20; var z = 40;", 550 | function() { 551 | var _ = $a; 552 | var _ = $b; 553 | }, { 554 | "varCallbacks": { 555 | "$a": function(obj) { 556 | return obj.type === "Literal" && obj.value > 11; 557 | }, 558 | "$b": function(obj) { 559 | return obj.type === "Literal" && obj.value < 11; 560 | } 561 | } 562 | }), 563 | false, "Two single-matching var callbacks still need ordering."); 564 | 565 | 566 | var varCallbacks; 567 | varCallbacks = { 568 | "$a": function(obj) { 569 | return { 570 | "failure": "Nothing can match $a!" 571 | }; 572 | } 573 | }; 574 | var result = Structured.match("var x = 10; var y = 20", 575 | function() { 576 | var $a = _; 577 | }, { 578 | "varCallbacks": varCallbacks 579 | }); 580 | equal(result, false, "Returning failure object will be false"); 581 | equal(varCallbacks.failure, "Nothing can match $a!", 582 | "Failure message is correctly set, basic."); 583 | 584 | varCallbacks = { 585 | "$a": function(obj) { 586 | return true; 587 | }, 588 | "$b": function(obj) { 589 | if (obj.value > 30) { 590 | return true; 591 | } 592 | return { 593 | "failure": "Make sure the value is big" 594 | } 595 | } 596 | }; 597 | var result = Structured.match("var x = 10; var y = 20; var c = 0;", 598 | function() { 599 | var $a = $b; 600 | }, { 601 | "varCallbacks": varCallbacks 602 | }); 603 | equal(result, false, "Returning failure object is false"); 604 | equal(varCallbacks.failure, "Make sure the value is big", 605 | "Failure message is correctly set, basic."); 606 | 607 | varCallbacks = { 608 | "$a": function(obj) { 609 | return true; 610 | }, 611 | "$b": function(obj) { 612 | if (obj.value > 90) { 613 | return true; 614 | } 615 | return { 616 | "failure": "Make sure the value is big" 617 | }; 618 | } 619 | }; 620 | var result = Structured.match("var x = 10; var y = 20; var c = 100;", 621 | function() { 622 | var $a = $b; 623 | }, { 624 | "varCallbacks": varCallbacks 625 | }); 626 | equal(!!result, true, "Matches still work around failure messages"); 627 | equal(varCallbacks.failure, undefined, 628 | "Failure message is not set if no failure."); 629 | }); 630 | 631 | test("More complicated single variable callbacks", function() { 632 | var code, structure, varCallbacks; 633 | structure = function() { 634 | var $a = _, 635 | _ = $val; 636 | var $d = function() {}; 637 | var draw = function() { 638 | var $b = $a + _; 639 | $d(_, $e, $b, _); 640 | $d($a); 641 | $a = $e.length; 642 | }; 643 | }; 644 | code = " \n \ 645 | var a = 10, z = 3, b = 20, y = 1, k = foo(); \n \ 646 | var bar = function(x) {return x + 3;}; \n \ 647 | var foo = function(x) {return x + 3;}; \n \ 648 | var draw = function() { \n \ 649 | var t = z + y; \n \ 650 | foo(t); \n \ 651 | foo(3, 'falcon', t, 10); \n \ 652 | foo(3, 'eagle', t, 10); \n \ 653 | test(z); \n \ 654 | foo(z); \n \ 655 | z = 'falcon'.length; \n \ 656 | z = 'eagle'.length; \n \ 657 | } \n \ 658 | "; 659 | var varCallbacks = { 660 | "$e": function(obj) { 661 | return obj.value === "eagle"; 662 | }, 663 | "$val": function(obj) { 664 | return (obj.type === "CallExpression" && 665 | obj.callee.name === "foo"); 666 | } 667 | }; 668 | equal(!!Structured.match(code, structure, { 669 | varCallbacks: varCallbacks 670 | }), 671 | true, "Complex with parameters, function names, etc works."); 672 | }); 673 | 674 | test("Multiple variable callbacks", function() { 675 | var varCallbacks, code, structure, result; 676 | varCallbacks = { 677 | "$a, $b": function(a, b) { 678 | return a.value > b.value; 679 | } 680 | }; 681 | result = !!Structured.match("var x = 50; var y = 20;", 682 | function() { 683 | var _ = $a; 684 | var _ = $b; 685 | }, { 686 | "varCallbacks": varCallbacks 687 | }); 688 | equal(result, true, "Simple multiple variable callback works."); 689 | 690 | 691 | varCallbacks = { 692 | "$a, $b": function(a, b) { 693 | return a.value > b.value; 694 | } 695 | }; 696 | result = !!Structured.match("var x = 50; var y = 20;", 697 | function() { 698 | var _ = $a; 699 | var _ = $b; 700 | }, { 701 | "varCallbacks": varCallbacks 702 | }); 703 | equal(result, true, "Simple multiple variable callback works."); 704 | 705 | 706 | varCallbacks = { 707 | "$a,$b, $c": function(a, b, c) { 708 | return a.value > b.value && c.value !== 40; 709 | } 710 | }; 711 | equal(!!Structured.match("var a = 3; var b = 1; var c = 4;", 712 | function() { 713 | var a = $a; 714 | var b = $b; 715 | var c = $c; 716 | }), 717 | true, "Trim from left works."); 718 | 719 | 720 | // TODO test more involved var callbacks. 721 | varCallbacks = { 722 | "$c, $a, $b": function(c, a, b) { 723 | return a.value > b.value; 724 | }, 725 | "$c": function(c) { 726 | return c.type === "Identifier" && c.name !== "foo"; 727 | }, 728 | "$c, $d, $e": function(c, d, e) { 729 | return d.value === e.value; 730 | } 731 | }; 732 | structure = function() { 733 | _ += $a + $b; 734 | $c($e, $d); 735 | }; 736 | code = "tree += 30 + 50 + 10; plant(40, 20); forest(30, 30);"; 737 | result = Structured.match(code, structure, { 738 | "varCallbacks": varCallbacks 739 | }); 740 | equal(!!result, true, "Multiple multiple-var callbacks work."); 741 | 742 | code = ("tree += 30 + 50 + 70; plant(40, 0) + forest(30, 30);" + 743 | "tree += 30 + 50 + 10; plant(40, 0) + forest(30, 60);"); 744 | result = Structured.match(code, structure, { 745 | "varCallbacks": varCallbacks, 746 | "orderMatters": true 747 | }); 748 | equal(result, false, "False multiple multiple-var callbacks work."); 749 | equal(varCallbacks.failure, undefined, 750 | "No failure message if none specified."); 751 | 752 | 753 | varCallbacks = { 754 | "$red, $green, $blue": function(red, green, blue) { 755 | if (red.value < 50) { 756 | return { 757 | "failure": "Red must be greater than 50" 758 | }; 759 | } 760 | if (green.value < blue.value) { 761 | return { 762 | "failure": "Use more green than blue" 763 | }; 764 | } 765 | return true; 766 | } 767 | }; 768 | structure = function() { 769 | fill($red, $green, $blue); 770 | }; 771 | result = Structured.match("var foo = 5; foo += 2; fill(100, 40, 200);", 772 | structure, { 773 | "varCallbacks": varCallbacks 774 | }); 775 | equal(result, false, "False RGB matching works."); 776 | equal(varCallbacks.failure, "Use more green than blue", 777 | "False RGB message works"); 778 | 779 | result = Structured.match("var foo = 5; foo += 2; fill(100, 40, 2);", 780 | structure, { 781 | "varCallbacks": varCallbacks 782 | }); 783 | equal(!!result, true, "True RGB matching works."); 784 | equal(varCallbacks.failure, undefined, "True RGB message works"); 785 | }); 786 | }; 787 | 788 | var constantFolding = function() { 789 | QUnit.module("Constant folding"); 790 | test("Simple constant folding", function() { 791 | ok(Structured.match("var x = -5;", 792 | function() { 793 | var x = $num; 794 | }, { 795 | "varCallbacks": { 796 | "$num": function(num) { 797 | return num && num.value && num.value < 0; 798 | } 799 | } 800 | }), 801 | "Unary - operator folded on number literals."); 802 | 803 | ok(Structured.match("var x = +5;", 804 | function() { 805 | var x = $num; 806 | }, { 807 | "varCallbacks": { 808 | "$num": function(num) { 809 | return num && num.value && num.value > -10; 810 | } 811 | } 812 | }), 813 | "Unary + operator folded on number literals."); 814 | 815 | ok(Structured.match("var y = 10; var x = +y; x = -y;", 816 | function() { 817 | var x = +$var; 818 | x = -$var; 819 | }), 820 | "Unary + - operators work on non-literals."); 821 | }); 822 | }; 823 | 824 | var wildcardVarTests = function() { 825 | QUnit.module("Wildcard variables"); 826 | test("Simple wildcard tests", function() { 827 | var structure, code; 828 | 829 | ok(Structured.match("var x = 10; x -= 1;", 830 | function() { 831 | var $a = 10; 832 | $a -= 1; 833 | }), 834 | "Basic variable match works."); 835 | 836 | equal(Structured.match("var x = 10; y -= 1;", 837 | function() { 838 | var $a = 10; 839 | $a -= 1; 840 | }), 841 | false, "Basic variable no-match works."); 842 | 843 | ok(Structured.match("var x = 10; var y = 10; var t = 3; var c = 1; c = 3; t += 2; y += 2;", 844 | function() { 845 | var $a = 10; 846 | var $b = _; 847 | $b += 2; 848 | $a += 2; 849 | }), 850 | "Basic multiple-option variable match works."); 851 | 852 | equal(Structured.match("var x = 10; var y = 10; var t = 3; var c = 1; c = 3; y += 2;", 853 | function() { 854 | var $a = _; 855 | var $b = _; 856 | $b = 3 + 2; 857 | $a += 2; 858 | }), false, 859 | "Basic multiple-option variable no-match works."); 860 | 861 | equal(!!Structured.match("if(true) {var x = 2;} var y = 4; z = x + y", 862 | function() { 863 | if (_) { 864 | var $a = _; 865 | } 866 | var $b = 4; 867 | _ = $a + $b; 868 | }), 869 | true, "Simple multiple var match works"); 870 | 871 | // QUnit test code 872 | structure = function() { 873 | function $k(bar) {} 874 | $k; 875 | }; 876 | code = "function foo(bar) {} bar; foo;"; 877 | equal(!!Structured.match(code, structure), 878 | true, "Matching declared function name works."); 879 | }); 880 | test("Involved wildcard tests", function() { 881 | var structure, code; 882 | structure = function() { 883 | var $a, $b, $c, $d; 884 | $a = 1; 885 | $b = 1; 886 | $c = 1; 887 | $d = 1; 888 | $a += 3; 889 | }; 890 | code = " \n \ 891 | var r, s, t, u, v, w, x, y, z; \n \ 892 | r = 1; s = 1; t = 1; u = 1; v = 1; w = 1; x = 1; y = 1; z = 1; \n \ 893 | u += 3; \n \ 894 | "; 895 | equal(!!Structured.match(code, structure), 896 | true, "More ambiguous multi-variable matching works."); 897 | code = " \n \ 898 | var r, s, t, u, v, w, x, y, z; \n \ 899 | r = 1; s = 1; t = 1; u = 1; v = 1; w = 1; x = 1; y = 1; z = 1; \n \ 900 | x += 3; \n \ 901 | "; 902 | equal(Structured.match(code, structure), 903 | false, "More ambiguous multi-variable non-matching works."); 904 | 905 | structure = function() { 906 | var $a = _; 907 | var $d = function() {} 908 | var draw = function() { 909 | var $b = $a + _; 910 | $d(_, $e, $b, _); 911 | $d($a); 912 | $a = $e.length; 913 | } 914 | }; 915 | code = " \n \ 916 | var a = 10, b = 20, z = 3, y = 1; \n \ 917 | var bar = function(x) {return x + 3;}; \n \ 918 | var foo = function(x) {return x + 3;}; \n \ 919 | var draw = function() { \n \ 920 | var t = z + y; \n \ 921 | foo(t); \n \ 922 | foo(3, 'eagle', t, 10); \n \ 923 | test(z); \n \ 924 | foo(z); \n \ 925 | z = 'eagle'.length; \n \ 926 | } \n \ 927 | "; 928 | equal(!!Structured.match(code, structure), 929 | true, "Complex vars with parameters, function names, etc works."); 930 | code = " \n \ 931 | var a = 10, b = 20, z = 3, y = 1; \n \ 932 | var bar = function(x) {return x + 3;}; \n \ 933 | var foo = function(x) {return x + 3;}; \n \ 934 | var draw = function() { \n \ 935 | var t = z + y; \n \ 936 | foo(t); \n \ 937 | foo(3, 'eagle', t, 10); \n \ 938 | test(z); \n \ 939 | foo(z); \n \ 940 | z = 'eaglee'.length; \n \ 941 | } \n \ 942 | "; 943 | equal(Structured.match(code, structure), 944 | false, "Complex vars with small mismatch breaks."); 945 | 946 | }); 947 | }; 948 | 949 | var nestingOrder = function() { 950 | QUnit.module("Nesting order"); 951 | /* 952 | * A structure implicitly applies a partial order constraint on 953 | * the nesting "levels" of expressions. In particular, if pattern 954 | * expression A lexically appears before pattern expression B, then 955 | * any binding of concrete expressions to A and B implies that B is 956 | * nested at the same or deeper level than A. 957 | */ 958 | test("Simple nesting tests", function() { 959 | var structure, code; 960 | 961 | ok(Structured.match("var x = 5; while(true) { var y = 6; }", 962 | function() { 963 | var x = 5; 964 | var y = 6; 965 | }), 966 | "Downward expression ordering works.") 967 | 968 | ok(!Structured.match("while(true) { var x = 5; } var y = 6;", 969 | function() { 970 | var x = 5; 971 | var y = 6; 972 | }), 973 | "Upward expression ordering fails.") 974 | }); 975 | }; 976 | 977 | var structureMatchTests = function() { 978 | QUnit.module("Structure Match Tests"); 979 | 980 | test("Match structure", function() { 981 | deepEqual(Structured.match("if(true){var x = 5;}", function() { 982 | var x = $a; 983 | }), { 984 | "_": [], 985 | "vars": { 986 | "a": { 987 | "raw": "5", 988 | "type": "Literal", 989 | "value": 5 990 | } 991 | }, 992 | "root": { 993 | "type": "VariableDeclaration", 994 | "declarations": [{ 995 | "type": "VariableDeclarator", 996 | "id": { 997 | "type": "Identifier", 998 | "name": "x" 999 | }, 1000 | "init": null 1001 | }], 1002 | "kind": "var" 1003 | } 1004 | }, "Verify variable match inside if statement."); 1005 | 1006 | deepEqual(Structured.match("if(true){var x = 5;}", function() { 1007 | var x = $a; 1008 | }), { 1009 | "_": [], 1010 | "vars": { 1011 | "a": { 1012 | "raw": "5", 1013 | "type": "Literal", 1014 | "value": 5 1015 | } 1016 | }, 1017 | "root": { 1018 | "type": "VariableDeclaration", 1019 | "declarations": [{ 1020 | "type": "VariableDeclarator", 1021 | "id": { 1022 | "type": "Identifier", 1023 | "name": "x" 1024 | }, 1025 | "init": null 1026 | }], 1027 | "kind": "var" 1028 | } 1029 | }, "Verify variable match inside if statement."); 1030 | 1031 | deepEqual(Structured.match("test(); if(true){var x = 5;}", { 1032 | "type": "Program", 1033 | "body": [{ 1034 | "type": "VariableDeclaration", 1035 | "declarations": [{ 1036 | "type": "VariableDeclarator", 1037 | "id": { 1038 | "type": "Identifier", 1039 | "name": "x" 1040 | }, 1041 | "init": { 1042 | "type": "Identifier", 1043 | "name": "_" 1044 | } 1045 | }], 1046 | "kind": "var" 1047 | }] 1048 | }), { 1049 | "_": [{ 1050 | "raw": "5", 1051 | "type": "Literal", 1052 | "value": 5 1053 | }], 1054 | "vars": {}, 1055 | "root": { 1056 | "type": "VariableDeclaration", 1057 | "declarations": [{ 1058 | "type": "VariableDeclarator", 1059 | "id": { 1060 | "type": "Identifier", 1061 | "name": "x" 1062 | }, 1063 | "init": null 1064 | }], 1065 | "kind": "var" 1066 | } 1067 | }, "Verify variable declaration with object AST."); 1068 | 1069 | deepEqual(Structured.match("test(); if(true){var x = 5;}", { 1070 | "type": "VariableDeclarator", 1071 | "id": { 1072 | "type": "Identifier", 1073 | "name": "x" 1074 | }, 1075 | "init": null 1076 | }), { 1077 | "_": [], 1078 | "vars": {}, 1079 | "root": { 1080 | "type": "VariableDeclarator", 1081 | "id": { 1082 | "type": "Identifier", 1083 | "name": "x" 1084 | }, 1085 | "init": null 1086 | } 1087 | }, "Verify primitive with simple object AST."); 1088 | 1089 | deepEqual(Structured.match("test(1,2,3);", function() { 1090 | _(); 1091 | }), { 1092 | "_": [{ 1093 | "type": "Identifier", 1094 | "name": "test" 1095 | }], 1096 | "vars": {}, 1097 | "root": { 1098 | "type": "ExpressionStatement", 1099 | "expression": { 1100 | "type": "CallExpression", 1101 | "callee": { 1102 | "type": "Identifier", 1103 | "name": "test" 1104 | }, 1105 | "arguments": [{ 1106 | "raw": "1", 1107 | "type": "Literal", 1108 | "value": 1 1109 | }, { 1110 | "raw": "2", 1111 | "type": "Literal", 1112 | "value": 2 1113 | }, { 1114 | "raw": "3", 1115 | "type": "Literal", 1116 | "value": 3 1117 | }] 1118 | } 1119 | } 1120 | }, "Verify function call matching."); 1121 | 1122 | deepEqual(Structured.match({ 1123 | "type": "VariableDeclaration", 1124 | "declarations": [{ 1125 | "type": "VariableDeclarator", 1126 | "id": { 1127 | "type": "Identifier", 1128 | "name": "x" 1129 | }, 1130 | "init": { 1131 | "raw": "5", 1132 | "type": "Literal", 1133 | "value": 5 1134 | } 1135 | }], 1136 | "kind": "var" 1137 | }, function() { 1138 | var x = $a; 1139 | }), { 1140 | "_": [], 1141 | "vars": {}, 1142 | "root": { 1143 | "type": "VariableDeclaration", 1144 | "declarations": [{ 1145 | "type": "VariableDeclarator", 1146 | "id": { 1147 | "type": "Identifier", 1148 | "name": "x" 1149 | }, 1150 | "init": null 1151 | }], 1152 | "kind": "var" 1153 | } 1154 | }, "Verify match of existing AST."); 1155 | 1156 | deepEqual(Structured.match( 1157 | "if(true){var a = 5; test();}", 1158 | function() { 1159 | if ($condition) { 1160 | glob$expressions; 1161 | } 1162 | }), { 1163 | "_": [], 1164 | "vars": { 1165 | "condition": { 1166 | "raw": "true", 1167 | "type": "Literal", 1168 | "value": true 1169 | }, 1170 | "expressions": [{ 1171 | "type": "VariableDeclaration", 1172 | "declarations": [{ 1173 | "type": "VariableDeclarator", 1174 | "id": { 1175 | "type": "Identifier", 1176 | "name": "a" 1177 | }, 1178 | "init": null 1179 | }], 1180 | "kind": "var" 1181 | }, { 1182 | "expression": { 1183 | "left": { 1184 | "name": "a", 1185 | "type": "Identifier" 1186 | }, 1187 | "operator": "=", 1188 | "right": { 1189 | "raw": "5", 1190 | "type": "Literal", 1191 | "value": 5 1192 | }, 1193 | "type": "AssignmentExpression" 1194 | }, 1195 | "type": "ExpressionStatement" 1196 | }, { 1197 | "type": "ExpressionStatement", 1198 | "expression": { 1199 | "type": "CallExpression", 1200 | "callee": { 1201 | "type": "Identifier", 1202 | "name": "test" 1203 | }, 1204 | "arguments": [] 1205 | } 1206 | }] 1207 | }, 1208 | "root": { 1209 | "type": "IfStatement", 1210 | "test": { 1211 | "raw": "true", 1212 | "type": "Literal", 1213 | "value": true 1214 | }, 1215 | "consequent": { 1216 | "type": "BlockStatement", 1217 | "body": [{ 1218 | "type": "VariableDeclaration", 1219 | "declarations": [{ 1220 | "type": "VariableDeclarator", 1221 | "id": { 1222 | "type": "Identifier", 1223 | "name": "a" 1224 | }, 1225 | "init": null 1226 | }], 1227 | "kind": "var" 1228 | }, { 1229 | "expression": { 1230 | "left": { 1231 | "name": "a", 1232 | "type": "Identifier" 1233 | }, 1234 | "operator": "=", 1235 | "right": { 1236 | "raw": "5", 1237 | "type": "Literal", 1238 | "value": 5 1239 | }, 1240 | "type": "AssignmentExpression" 1241 | }, 1242 | "type": "ExpressionStatement" 1243 | }, { 1244 | "type": "ExpressionStatement", 1245 | "expression": { 1246 | "type": "CallExpression", 1247 | "callee": { 1248 | "type": "Identifier", 1249 | "name": "test" 1250 | }, 1251 | "arguments": [] 1252 | } 1253 | }] 1254 | }, 1255 | "alternate": null 1256 | } 1257 | }, 1258 | "Test glob expressions." 1259 | ); 1260 | 1261 | deepEqual(Structured.match( 1262 | "if(true){}", 1263 | function() { 1264 | if ($condition) { 1265 | glob$expressions; 1266 | } 1267 | }), { 1268 | "_": [], 1269 | "vars": { 1270 | "condition": { 1271 | "raw": "true", 1272 | "type": "Literal", 1273 | "value": true 1274 | }, 1275 | "expressions": [] 1276 | }, 1277 | "root": { 1278 | "type": "IfStatement", 1279 | "test": { 1280 | "raw": "true", 1281 | "type": "Literal", 1282 | "value": true 1283 | }, 1284 | "consequent": { 1285 | "type": "BlockStatement", 1286 | "body": [] 1287 | }, 1288 | "alternate": null 1289 | } 1290 | }, 1291 | "Test empty glob expressions." 1292 | ); 1293 | 1294 | deepEqual(Structured.match("rect(10,20,200,200,300,400);", 1295 | function() { 1296 | rect(_, _, _, _, glob_); 1297 | }), { 1298 | "_": [{ 1299 | "raw": "10", 1300 | "type": "Literal", 1301 | "value": 10 1302 | }, { 1303 | "raw": "20", 1304 | "type": "Literal", 1305 | "value": 20 1306 | }, { 1307 | "raw": "200", 1308 | "type": "Literal", 1309 | "value": 200 1310 | }, { 1311 | "raw": "200", 1312 | "type": "Literal", 1313 | "value": 200 1314 | }, 1315 | [{ 1316 | "raw": "300", 1317 | "type": "Literal", 1318 | "value": 300 1319 | }, { 1320 | "raw": "400", 1321 | "type": "Literal", 1322 | "value": 400 1323 | }] 1324 | ], 1325 | "vars": {}, 1326 | "root": { 1327 | "type": "ExpressionStatement", 1328 | "expression": { 1329 | "type": "CallExpression", 1330 | "callee": { 1331 | "type": "Identifier", 1332 | "name": "rect" 1333 | }, 1334 | "arguments": [{ 1335 | "raw": "10", 1336 | "type": "Literal", 1337 | "value": 10 1338 | }, { 1339 | "raw": "20", 1340 | "type": "Literal", 1341 | "value": 20 1342 | }, { 1343 | "raw": "200", 1344 | "type": "Literal", 1345 | "value": 200 1346 | }, { 1347 | "raw": "200", 1348 | "type": "Literal", 1349 | "value": 200 1350 | }, { 1351 | "raw": "300", 1352 | "type": "Literal", 1353 | "value": 300 1354 | }, { 1355 | "raw": "400", 1356 | "type": "Literal", 1357 | "value": 400 1358 | }] 1359 | } 1360 | } 1361 | }, "Glob additional arguments"); 1362 | 1363 | deepEqual(Structured.match("rect(10,20,200,200);", 1364 | function() { 1365 | rect(_, _, _, _, glob_); 1366 | }), { 1367 | "_": [{ 1368 | "raw": "10", 1369 | "type": "Literal", 1370 | "value": 10 1371 | }, { 1372 | "raw": "20", 1373 | "type": "Literal", 1374 | "value": 20 1375 | }, { 1376 | "raw": "200", 1377 | "type": "Literal", 1378 | "value": 200 1379 | }, { 1380 | "raw": "200", 1381 | "type": "Literal", 1382 | "value": 200 1383 | }, 1384 | [] 1385 | ], 1386 | "vars": {}, 1387 | "root": { 1388 | "type": "ExpressionStatement", 1389 | "expression": { 1390 | "type": "CallExpression", 1391 | "callee": { 1392 | "type": "Identifier", 1393 | "name": "rect" 1394 | }, 1395 | "arguments": [{ 1396 | "raw": "10", 1397 | "type": "Literal", 1398 | "value": 10 1399 | }, { 1400 | "raw": "20", 1401 | "type": "Literal", 1402 | "value": 20 1403 | }, { 1404 | "raw": "200", 1405 | "type": "Literal", 1406 | "value": 200 1407 | }, { 1408 | "raw": "200", 1409 | "type": "Literal", 1410 | "value": 200 1411 | }] 1412 | } 1413 | } 1414 | }, "Glob additional arguments, empty"); 1415 | 1416 | deepEqual(Structured.match("rect(10,20,200,200);", 1417 | function() { 1418 | rect($x, $y, $w, $h); 1419 | }), { 1420 | "_": [], 1421 | "vars": { 1422 | "x": { 1423 | "raw": "10", 1424 | "type": "Literal", 1425 | "value": 10 1426 | }, 1427 | "y": { 1428 | "raw": "20", 1429 | "type": "Literal", 1430 | "value": 20 1431 | }, 1432 | "w": { 1433 | "raw": "200", 1434 | "type": "Literal", 1435 | "value": 200 1436 | }, 1437 | "h": { 1438 | "raw": "200", 1439 | "type": "Literal", 1440 | "value": 200 1441 | } 1442 | }, 1443 | "root": { 1444 | "type": "ExpressionStatement", 1445 | "expression": { 1446 | "type": "CallExpression", 1447 | "callee": { 1448 | "type": "Identifier", 1449 | "name": "rect" 1450 | }, 1451 | "arguments": [{ 1452 | "raw": "10", 1453 | "type": "Literal", 1454 | "value": 10 1455 | }, { 1456 | "raw": "20", 1457 | "type": "Literal", 1458 | "value": 20 1459 | }, { 1460 | "raw": "200", 1461 | "type": "Literal", 1462 | "value": 200 1463 | }, { 1464 | "raw": "200", 1465 | "type": "Literal", 1466 | "value": 200 1467 | }] 1468 | } 1469 | } 1470 | }); 1471 | 1472 | deepEqual(Structured.match("var a = function(){};", 1473 | function() { 1474 | var _ = _; 1475 | }), { 1476 | "_": [{ 1477 | "type": "Identifier", 1478 | "name": "a" 1479 | }, { 1480 | "type": "Identifier", 1481 | "name": "a" 1482 | }, { 1483 | "type": "FunctionExpression", 1484 | "id": null, 1485 | "params": [], 1486 | "defaults": [], 1487 | "body": { 1488 | "type": "BlockStatement", 1489 | "body": [] 1490 | }, 1491 | "rest": null, 1492 | "generator": false, 1493 | "expression": false 1494 | }], 1495 | "vars": {}, 1496 | "root": { 1497 | "type": "VariableDeclaration", 1498 | "declarations": [{ 1499 | "type": "VariableDeclarator", 1500 | "id": { 1501 | "type": "Identifier", 1502 | "name": "a" 1503 | }, 1504 | "init": null 1505 | }], 1506 | "kind": "var" 1507 | } 1508 | }); 1509 | 1510 | deepEqual(Structured.match("[,]", 1511 | function() { 1512 | [_] 1513 | }), { 1514 | "_": [null], 1515 | "vars": {}, 1516 | "root":{ 1517 | "type": "ExpressionStatement", 1518 | "expression": { 1519 | "type": "ArrayExpression", 1520 | "elements": [ 1521 | null 1522 | ] 1523 | } 1524 | }, 1525 | "vars": {} 1526 | }, "Capturing [,]"); 1527 | 1528 | var ifBlock = { 1529 | "type": "IfStatement", 1530 | "test": { 1531 | "raw": "true", 1532 | "type": "Literal", 1533 | "value": true 1534 | }, 1535 | "consequent": { 1536 | "type": "BlockStatement", 1537 | "body": [{ 1538 | "type": "VariableDeclaration", 1539 | "declarations": [{ 1540 | "type": "VariableDeclarator", 1541 | "id": { 1542 | "type": "Identifier", 1543 | "name": "a" 1544 | }, 1545 | "init": null 1546 | }], 1547 | "kind": "var" 1548 | }, { 1549 | "type": "ExpressionStatement", 1550 | "expression": { 1551 | "type": "CallExpression", 1552 | "callee": { 1553 | "type": "Identifier", 1554 | "name": "test" 1555 | }, 1556 | "arguments": [] 1557 | } 1558 | }] 1559 | }, 1560 | "alternate": null 1561 | }; 1562 | 1563 | deepEqual(Structured.matchNode(ifBlock, { 1564 | "type": "IfStatement", 1565 | "test": { 1566 | "raw": "true", 1567 | "type": "Literal", 1568 | "value": true 1569 | }, 1570 | "consequent": { 1571 | "type": "BlockStatement", 1572 | "body": [] 1573 | }, 1574 | "alternate": null 1575 | }), { 1576 | "_": [], 1577 | "vars": {}, 1578 | "root": ifBlock 1579 | }, "Verify exact node match."); 1580 | 1581 | deepEqual(Structured.matchNode(ifBlock, function() { 1582 | if (true) {} 1583 | }), { 1584 | "_": [], 1585 | "vars": {}, 1586 | "root": ifBlock 1587 | }, "Verify exact node match."); 1588 | 1589 | deepEqual(Structured.matchNode(ifBlock, function() { 1590 | if (true) { 1591 | glob$statements; 1592 | } 1593 | }), { 1594 | "_": [], 1595 | "vars": { 1596 | "statements": [{ 1597 | "type": "VariableDeclaration", 1598 | "declarations": [{ 1599 | "type": "VariableDeclarator", 1600 | "id": { 1601 | "type": "Identifier", 1602 | "name": "a" 1603 | }, 1604 | "init": null 1605 | }], 1606 | "kind": "var" 1607 | }, { 1608 | "type": "ExpressionStatement", 1609 | "expression": { 1610 | "type": "CallExpression", 1611 | "callee": { 1612 | "type": "Identifier", 1613 | "name": "test" 1614 | }, 1615 | "arguments": [] 1616 | } 1617 | }] 1618 | }, 1619 | "root": ifBlock 1620 | }, "Verify exact node match."); 1621 | 1622 | deepEqual(Structured.matchNode(ifBlock, function() { 1623 | test(); 1624 | }), false, "Verify in-exact node match fails."); 1625 | 1626 | deepEqual(Structured.matchNode(ifBlock, { 1627 | "raw": "true", 1628 | "type": "Literal", 1629 | "value": true 1630 | }), false, "Verify in-exact node match fails."); 1631 | }); 1632 | }; 1633 | 1634 | var injectDataTests = function() { 1635 | QUnit.module("Injecting Data"); 1636 | 1637 | test("Injecting data", function() { 1638 | deepEqual(Structured.injectData(function() { 1639 | if ($condition) { 1640 | glob$expressions; 1641 | } 1642 | }, { 1643 | vars: { 1644 | condition: { 1645 | "raw": "true", 1646 | "type": "Literal", 1647 | "value": true 1648 | }, 1649 | expressions: [{ 1650 | "type": "ExpressionStatement", 1651 | "expression": { 1652 | "type": "CallExpression", 1653 | "callee": { 1654 | "type": "Identifier", 1655 | "name": "test" 1656 | }, 1657 | "arguments": [] 1658 | } 1659 | }] 1660 | } 1661 | }), { 1662 | "type": "BlockStatement", 1663 | "body": [{ 1664 | "type": "IfStatement", 1665 | "test": { 1666 | "raw": "true", 1667 | "type": "Literal", 1668 | "value": true 1669 | }, 1670 | "consequent": { 1671 | "type": "BlockStatement", 1672 | "body": [{ 1673 | "type": "ExpressionStatement", 1674 | "expression": { 1675 | "type": "CallExpression", 1676 | "callee": { 1677 | "type": "Identifier", 1678 | "name": "test" 1679 | }, 1680 | "arguments": [] 1681 | } 1682 | }] 1683 | }, 1684 | "alternate": null 1685 | }] 1686 | }, "Inject a regular and glob variable."); 1687 | 1688 | deepEqual(Structured.injectData(function() { 1689 | if ($condition) { 1690 | oldTest(); 1691 | glob$expressions; 1692 | } 1693 | }, { 1694 | vars: { 1695 | condition: { 1696 | "raw": "true", 1697 | "type": "Literal", 1698 | "value": true 1699 | }, 1700 | expressions: [{ 1701 | "type": "ExpressionStatement", 1702 | "expression": { 1703 | "type": "CallExpression", 1704 | "callee": { 1705 | "type": "Identifier", 1706 | "name": "test1" 1707 | }, 1708 | "arguments": [] 1709 | } 1710 | }, { 1711 | "type": "ExpressionStatement", 1712 | "expression": { 1713 | "type": "CallExpression", 1714 | "callee": { 1715 | "type": "Identifier", 1716 | "name": "test2" 1717 | }, 1718 | "arguments": [] 1719 | } 1720 | }] 1721 | } 1722 | }), { 1723 | "type": "BlockStatement", 1724 | "body": [{ 1725 | "type": "IfStatement", 1726 | "test": { 1727 | "raw": "true", 1728 | "type": "Literal", 1729 | "value": true 1730 | }, 1731 | "consequent": { 1732 | "type": "BlockStatement", 1733 | "body": [{ 1734 | "type": "ExpressionStatement", 1735 | "expression": { 1736 | "type": "CallExpression", 1737 | "callee": { 1738 | "type": "Identifier", 1739 | "name": "oldTest" 1740 | }, 1741 | "arguments": [] 1742 | } 1743 | }, { 1744 | "type": "ExpressionStatement", 1745 | "expression": { 1746 | "type": "CallExpression", 1747 | "callee": { 1748 | "type": "Identifier", 1749 | "name": "test1" 1750 | }, 1751 | "arguments": [] 1752 | } 1753 | }, { 1754 | "type": "ExpressionStatement", 1755 | "expression": { 1756 | "type": "CallExpression", 1757 | "callee": { 1758 | "type": "Identifier", 1759 | "name": "test2" 1760 | }, 1761 | "arguments": [] 1762 | } 1763 | }] 1764 | }, 1765 | "alternate": null 1766 | }] 1767 | }, "Test inserting multiple glob after."); 1768 | }); 1769 | }; 1770 | 1771 | var altVarCallbacks = function(){ 1772 | QUnit.module("Better varCallbacks Formats"); 1773 | 1774 | test("--", function() { 1775 | ok(Structured.match("var a = 1", "function(){ var $v = 1; }", {varCallbacks: function ($v) { 1776 | return $v.name === "a"; 1777 | }}), "Single function") 1778 | 1779 | ok(!Structured.match("var a = 1", "function(){ var $v = 1; }", {varCallbacks: function ($v) { 1780 | return $v.name === "ab"; 1781 | }}), "Single function failure.") 1782 | 1783 | ok(Structured.match("var a = 2", "function(){ var _ = $x; }", {varCallbacks: [function ($x) { 1784 | return $x.value === 2; 1785 | }]}), "List of functions") 1786 | 1787 | ok(!Structured.match("var a = 2", "function(){ var _ = $x; }", {varCallbacks: [function ($x) { 1788 | return $x.value === 3; 1789 | }]}), "List of functions failure") 1790 | 1791 | ok(Structured.match("size()", "function(){ $f() }", {varCallbacks: { 1792 | variables: ["$f"], 1793 | fn: function (func) { 1794 | return func.name === "size"; 1795 | } 1796 | }}), "Single constraint") 1797 | 1798 | ok(!Structured.match("size()", "function(){ $f() }", {varCallbacks: { 1799 | variables: ["$f"], 1800 | fn: function (func) { 1801 | return func.name === "sizzle"; 1802 | } 1803 | }}), "Single constraint failure") 1804 | 1805 | ok(Structured.match("a = b + 10", "function(){ a = $fun + $add }", {varCallbacks: [{ 1806 | variables: ["$fun"], 1807 | fn: function (variable) { 1808 | return variable.name === "b"; 1809 | } 1810 | },{ 1811 | variables: ["$add"], 1812 | fn: function (num) { 1813 | return num.value > 5; 1814 | } 1815 | }]}), "List of Constraints") 1816 | 1817 | ok(Structured.match("a = b + 10", "function(){ a = $fun + $add }", {varCallbacks: [function($fun){ 1818 | return $fun.name == "b"; 1819 | },{ 1820 | variables: ["$add"], 1821 | fn: function (num) { 1822 | return num.value > 5; 1823 | } 1824 | }]}), "List of Constraints") 1825 | }); 1826 | }; 1827 | 1828 | var commutativity = function(){ 1829 | QUnit.module("Checking basic commutative equivalence"); 1830 | 1831 | test("--", function() { 1832 | structure = function() { 1833 | $a = $a + 1; 1834 | }; 1835 | code = "a += 1;"; 1836 | equal(!!Structured.match(code, structure, {editorCallbacks: {}}), true, "match += to = +"); 1837 | 1838 | structure = function() { 1839 | $a += 1; 1840 | }; 1841 | code = "a = a + 1;"; 1842 | equal(!!Structured.match(code, structure, {editorCallbacks: {}}), true, "match = + to +="); 1843 | 1844 | structure = function() { 1845 | $a + 7; 1846 | }; 1847 | code = "7 + a;"; 1848 | equal(!!Structured.match(code, structure, {editorCallbacks: {}}), true, "commutative property of addition"); 1849 | 1850 | structure = function() { 1851 | 7 * $a; 1852 | }; 1853 | code = "a * 7;"; 1854 | equal(!!Structured.match(code, structure, {editorCallbacks: {}}), true, "commutative property of multiplication"); 1855 | 1856 | structure = function() { 1857 | 7 < $a; 1858 | }; 1859 | code = "a > 7;"; 1860 | equal(!!Structured.match(code, structure, {editorCallbacks: {}}), true, "a > 7 matches 7 < a"); 1861 | 1862 | structure = function() { 1863 | 7 > $a; 1864 | }; 1865 | code = "a < 7;"; 1866 | equal(!!Structured.match(code, structure, {editorCallbacks: {}}), true, "a < 7 matches 7 > a"); 1867 | 1868 | structure = function() { 1869 | 7 <= $a; 1870 | }; 1871 | code = "a >= 7;"; 1872 | equal(!!Structured.match(code, structure, {editorCallbacks: {}}), true, "a >= 7 matches 7 <= a"); 1873 | 1874 | structure = function() { 1875 | 7 >= $a; 1876 | }; 1877 | code = "a <= 7;"; 1878 | equal(!!Structured.match(code, structure, {editorCallbacks: {}}), true, "a <= 7 matches 7 >= a"); 1879 | 1880 | structure = function() { 1881 | $a += 1; 1882 | }; 1883 | code = "a++;"; 1884 | equal(!!Structured.match(code, structure, {editorCallbacks: {}}), true, "a++ matches a += 1"); 1885 | 1886 | structure = function() { 1887 | $a -= 1; 1888 | }; 1889 | code = "a--;"; 1890 | equal(!!Structured.match(code, structure, {editorCallbacks: {}}), true, "a-- matches a -= 1"); 1891 | 1892 | structure = function() { 1893 | true && false; 1894 | }; 1895 | code = "false && true;"; 1896 | equal(!!Structured.match(code, structure, {editorCallbacks: {}}), true, "true && false matches false && true"); 1897 | 1898 | structure = function() { 1899 | ($a === 3) || true; 1900 | }; 1901 | code = "true || (a === 3);"; 1902 | equal(!!Structured.match(code, structure, {editorCallbacks: {}}), true, "true || (a === 3) matches (a === 3) || true"); 1903 | 1904 | structure = function() { 1905 | $a === 7; 1906 | }; 1907 | code = "7 === a;"; 1908 | equal(!!Structured.match(code, structure, {editorCallbacks: {}}), true, "commutative property of ==="); 1909 | 1910 | structure = function() { 1911 | 7 != $a; 1912 | }; 1913 | code = "a != 7;"; 1914 | equal(!!Structured.match(code, structure, {editorCallbacks: {}}), true, "commutative property of !="); 1915 | 1916 | structure = function() { 1917 | $a !== 7; 1918 | }; 1919 | code = "7 !== a;"; 1920 | equal(!!Structured.match(code, structure, {editorCallbacks: {}}), true, "commutative property of !=="); 1921 | 1922 | structure = function() { 1923 | 7 == $a; 1924 | }; 1925 | code = "a == 7;"; 1926 | equal(!!Structured.match(code, structure, {editorCallbacks: {}}), true, "commutative property of =="); 1927 | 1928 | structure = function() { 1929 | $a & 7; 1930 | }; 1931 | code = "7 & a;"; 1932 | equal(!!Structured.match(code, structure, {editorCallbacks: {}}), true, "commutative property of &"); 1933 | 1934 | structure = function() { 1935 | 7 | $a; 1936 | }; 1937 | code = "a | 7;"; 1938 | equal(!!Structured.match(code, structure, {editorCallbacks: {}}), true, "commutative property of |"); 1939 | 1940 | structure = function() { 1941 | $a ^ 7; 1942 | }; 1943 | code = "7 ^ a;"; 1944 | equal(!!Structured.match(code, structure, {editorCallbacks: {}}), true, "commutative property of ^"); 1945 | 1946 | structure = function() { 1947 | 7 && $a; 1948 | }; 1949 | code = "a && 7;"; 1950 | equal(!!Structured.match(code, structure, {editorCallbacks: {}}), true, "commutative property of &&"); 1951 | 1952 | structure = function() { 1953 | $a || 7; 1954 | }; 1955 | code = "7 || a;"; 1956 | equal(!!Structured.match(code, structure, {editorCallbacks: {}}), true, "commutative property of ||"); 1957 | 1958 | structure = function() { 1959 | var $a = 7; 1960 | }; 1961 | code = "var a; a = 7;"; 1962 | equal(!!Structured.match(code, structure, {editorCallbacks: {}}), true, "var a = 7 matches var a; a = 7"); 1963 | }); 1964 | }; 1965 | 1966 | var runAll = function() { 1967 | basicTests(); 1968 | clutterTests(); 1969 | nestedTests(); 1970 | drawingTests(); 1971 | peerTests(); 1972 | wildcardVarTests(); 1973 | varCallbackTests(); 1974 | constantFolding(); 1975 | combinedTests(); 1976 | nestingOrder(); 1977 | structureMatchTests(); 1978 | injectDataTests(); 1979 | altVarCallbacks(); 1980 | commutativity(); 1981 | }; 1982 | 1983 | runAll(); 1984 | --------------------------------------------------------------------------------