├── .editorconfig ├── .gitignore ├── .npmignore ├── .prettierrc.yaml ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── eslint.config.mjs ├── lib ├── main.d.ts ├── main.js ├── main.test-d.ts ├── parse.js └── sync ├── package-lock.json ├── package.json └── test └── main.test.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | 7 | [*.js] 8 | charset = utf-8 9 | indent_style = tab 10 | 11 | [{.travis.yml,package.json}] 12 | indent_style = space 13 | indent_size = 2 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | 10 | pids 11 | logs 12 | results 13 | 14 | npm-debug.log 15 | node_modules 16 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | 10 | pids 11 | logs 12 | results 13 | 14 | examples 15 | node_modules 16 | npm-debug.log 17 | .idea -------------------------------------------------------------------------------- /.prettierrc.yaml: -------------------------------------------------------------------------------- 1 | trailingComma: es5 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | dist: focal 3 | jobs: 4 | include: 5 | - node_js: stable 6 | env: COMMAND=lint 7 | - node_js: '12' 8 | env: COMMAND=test 9 | - node_js: '14' 10 | env: COMMAND=test 11 | - node_js: '16' 12 | env: COMMAND=test 13 | script: 14 | - npm run "$COMMAND" 15 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### 1.4.3 2 | 3 | Disallow access to prototype chain (CVE-2024-54152) when using compile with locals (two arguments in the called function) : 4 | 5 | ```js 6 | compile("__proto__")({}, {}); 7 | ``` 8 | 9 | => This now returns undefined, previously it would give you the `__proto__` instance which would allow Remote Code Execution. 10 | 11 | Thanks to [@JorianWoltjer](https://github.com/JorianWoltjer) who found the vulnerability and reported it. 12 | 13 | ### 1.4.2 14 | 15 | Make `handleThis` the default if you use the `Lexer` and `Parser` directly, and you don't use `.compile`. 16 | 17 | This is a way less common use case but it makes sense to have handleThis be the same default for both cases. 18 | 19 | (This also makes the library behave in the same way between 1.3.0 and 1.4.1 when using Parser or Lexer). There was a backwards incompatible change brought by 1.4.0 for users of `Parser`. 20 | 21 | ### 1.4.1 22 | 23 | Don't use this version, it is missing a commit for the 1.4.2 fix 24 | 25 | ### 1.4.0 26 | 27 | Add support for `handleThis: false` to disable handling of this. 28 | 29 | (By default handleThis is true). 30 | 31 | This way, if you write : `{this | filter}`, the `this` will be used as a key 32 | from the scope, eg `scope["this"]`. 33 | 34 | ### 1.3.0 35 | 36 | Add support for template literals. 37 | 38 | It is now possible to write : 39 | 40 | ```js 41 | compile("`Hello ${user}`")({ user: "John" }); 42 | // Returns "Hello John" 43 | ``` 44 | 45 | ### 1.2.1 46 | 47 | Bugfix `compile(tag, { csp: true })` should now work correctly. 48 | 49 | ### 1.2.0 50 | 51 | Add four options to the second arg of the compile method : 52 | 53 | - `compile(tag, {filters: { upper: (input) => input.toUpperCase()}})` which adds filters to a specific instance (those filters are not shared between instances). 54 | 55 | - `compile(tag, {cache: {}})` to set a "non global" cache. 56 | 57 | - `compile(tag, { csp: true })` to use the interpreter (avoid use of "new Function()" which is for example not allowed in Vercel). 58 | 59 | - `compile(tag, {literals: { true: true, false: false, null: null, undefined: undefined } })` which allows to customize literals (such as null, true, false, undefined) 60 | 61 | ### 1.1.10 62 | 63 | Update typescript typings for "Parser" 64 | 65 | ### 1.1.9 66 | 67 | Update typescript typings (add `.assign` method) 68 | 69 | ### 1.1.8 70 | 71 | Update typescript typings (add filters). 72 | 73 | ### 1.1.7 74 | 75 | Add typescript typings (for compile, Parser and Lexer). 76 | 77 | ### 1.1.6 78 | 79 | Published by mistake (same as 1.1.7), but without dependency changes 80 | 81 | ### 1.1.5 82 | 83 | Add specific error when a filter is not defined. 84 | 85 | ### 1.1.4 86 | 87 | Bugfix : When using an assignment expression, such as `b = a`, the value will always be set in the scope, not in the locals. 88 | 89 | With this code : 90 | 91 | ```js 92 | const scope = { a: 10 }; 93 | const locals = { b: 5 }; 94 | compile("b=a")(scope, locals); 95 | ``` 96 | 97 | The scope value will be `{ a: 10, b: 10 }` after the evaluation. 98 | 99 | In previous versions, the value would be assigned to the locals, meaning locals would be `{ b: 10 }` 100 | 101 | ### 1.1.3 102 | 103 | Bugfix : Make module ES5 compatible (to work in IE10 for example), by using var instead of const 104 | 105 | ### 1.1.2 106 | 107 | - Disallow access to prototype chain (CVE-2021-21277) 108 | 109 | ### 1.1.1 110 | 111 | Previous version was published with ES6 feature, now the published JS uses ES5 only 112 | 113 | ### 1.1.0 114 | 115 | - Add support for special characters by using the following : 116 | 117 | ```javascript 118 | function validChars(ch) { 119 | return ( 120 | (ch >= "a" && ch <= "z") || 121 | (ch >= "A" && ch <= "Z") || 122 | ch === "_" || 123 | ch === "$" || 124 | "ÀÈÌÒÙàèìòùÁÉÍÓÚáéíóúÂÊÎÔÛâêîôûÃÑÕãñõÄËÏÖÜŸäëïöüÿß".indexOf(ch) !== -1 125 | ); 126 | } 127 | evaluate = compile("être_embarassé", { 128 | isIdentifierStart: validChars, 129 | isIdentifierContinue: validChars, 130 | }); 131 | 132 | evaluate({ être_embarassé: "Ping" }); 133 | ``` 134 | 135 | ### 1.0.1 136 | 137 | - Disallow access to prototype chain (CVE-2020-5219) 138 | 139 | ### 1.0.0 140 | 141 | - Add support for `this` keyword to write : 142 | 143 | ```javascript 144 | evaluate = compile("this + 2")(2); // which gives 4 145 | ``` 146 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # angular-expressions 2 | 3 | **[angular's nicest part](https://github.com/angular/angular.js/blob/6b049c74ccc9ee19688bb9bbe504c300e61776dc/src/ng/parse.js) extracted as a standalone module for the browser and node.** 4 | 5 | [![build status](https://travis-ci.org/peerigon/angular-expressions.svg)](http://travis-ci.org/peerigon/angular-expressions) 6 | 7 | **angular-expressions** exposes a `.compile()`-method which can be used to compile evaluable expressions: 8 | 9 | ```javascript 10 | var expressions = require("angular-expressions"); 11 | 12 | evaluate = expressions.compile("1 + 1"); 13 | evaluate(); // returns 2 14 | ``` 15 | 16 | You can also set and get values on a given `scope`: 17 | 18 | ```javascript 19 | evaluate = expressions.compile("name"); 20 | scope = { name: "Jenny" }; 21 | evaluate(scope); // returns 'Jenny' 22 | 23 | evaluate = expressions.compile("ship.pirate.name = 'Störtebeker'"); 24 | evaluate(scope); // won't throw an error because angular's expressions are forgiving 25 | console.log(scope.ship.pirate.name); // prints 'Störtebeker' 26 | ``` 27 | 28 | For assigning values, you can also use `.assign()`: 29 | 30 | ```javascript 31 | evaluate = expressions.compile("ship.pirate.name"); 32 | evaluate.assign(scope, "Störtebeker"); 33 | console.log(scope.ship.pirate.name); // prints 'Störtebeker' 34 | ``` 35 | 36 | Check out [their readme](http://docs.angularjs.org/guide/expression) for further information. 37 | 38 |
39 | 40 | ## Setup 41 | 42 | [![npm status](https://nodei.co/npm/angular-expressions.svg?downloads=true&stars=true&downloadRank=true)](https://npmjs.org/package/angular-expressions) 43 | 44 |
45 | 46 | ## Filters 47 | 48 | Angular provides a mechanism to define filters on expressions: 49 | 50 | ```javascript 51 | expressions.filters.uppercase = (input) => input.toUpperCase(); 52 | 53 | expr = expressions.compile("'arr' | uppercase"); 54 | expr(); // returns 'ARR' 55 | ``` 56 | 57 | Arguments are evaluated against the scope: 58 | 59 | ```javascript 60 | expressions.filters.currency = (input, currency, digits) => { 61 | input = input.toFixed(digits); 62 | 63 | if (currency === "EUR") { 64 | return input + "€"; 65 | } else { 66 | return input + "$"; 67 | } 68 | }; 69 | 70 | expr = expressions.compile("1.2345 | currency:selectedCurrency:2"); 71 | expr({ 72 | selectedCurrency: "EUR", 73 | }); // returns '1.23€' 74 | ``` 75 | 76 | If you need an isolated `filters` object, this can be achieved by setting the `filters` attribute in the `options` argument. Global cache is disabled if using `options.filters`. To setup an isolated cache, you can also set the `cache` attribute in the `options` argument: 77 | 78 | ```javascript 79 | var isolatedFilters = { 80 | transform: (input) => input.toLowerCase(), 81 | }; 82 | var isolatedCache = {}; 83 | 84 | var resultOne = expressions.compile("'Foo Bar' | transform", { 85 | filters: isolatedFilters, 86 | cache: isolatedCache, 87 | }); 88 | 89 | console.log(resultOne()); // prints 'foo bar' 90 | console.log(isolatedCache); // prints '{"'Foo Bar' | transform": [Function fn] }' 91 | ``` 92 | 93 |
94 | 95 | ## API 96 | 97 | ### exports 98 | 99 | #### .compile(src): Function 100 | 101 | Compiles `src` and returns a function `evaluate()`. The compiled function is cached under `compile.cache[src]` to speed up further calls. 102 | 103 | Compiles also export the AST. 104 | 105 | Example output of: `compile("tmp + 1").ast` 106 | 107 | ``` 108 | { type: 'Program', 109 | body: 110 | [ { type: 'ExpressionStatement', 111 | expression: 112 | { type: 'Identifier', 113 | name: 'tmp', 114 | constant: false, 115 | toWatch: [ [Circular] ] } } ], 116 | constant: false } 117 | ``` 118 | 119 | _NOTE_ angular \$parse do not export ast variable it's done by this library. 120 | 121 | #### .compile.cache = Object.create(null) 122 | 123 | A cache containing all compiled functions. The src is used as key. Set this on `false` to disable the cache. 124 | 125 | #### .filters = {} 126 | 127 | An empty object where you may define your custom filters. 128 | 129 | #### .Lexer 130 | 131 | The internal [Lexer](https://github.com/angular/angular.js/blob/6b049c74ccc9ee19688bb9bbe504c300e61776dc/src/ng/parse.js#L116). 132 | 133 | #### .Parser 134 | 135 | The internal [Parser](https://github.com/angular/angular.js/blob/6b049c74ccc9ee19688bb9bbe504c300e61776dc/src/ng/parse.js#L390). 136 | 137 | --- 138 | 139 | ### evaluate(scope?): \* 140 | 141 | Evaluates the compiled `src` and returns the result of the expression. Property look-ups or assignments are executed on a given `scope`. 142 | 143 | ### evaluate.assign(scope, value): \* 144 | 145 | Tries to assign the given `value` to the result of the compiled expression on the given `scope` and returns the result of the assignment. 146 | 147 |
148 | 149 | ## In the browser 150 | 151 | There is no `dist` build because it's not 2005 anymore. Use a module bundler like [webpack](http://webpack.github.io/) or [browserify](http://browserify.org/). They're both capable of CommonJS and AMD. 152 | 153 |
154 | 155 | ## Security 156 | 157 | The code of angular was not secured from reading prototype, and since version 1.0.1 of angular-expressions, the module disallows reading properties that are not ownProperties. See [this blog post](http://blog.angularjs.org/2016/09/angular-16-expression-sandbox-removal.html) for more details about the sandbox that got removed completely in angular 1.6. 158 | 159 | Comment from `angular.js/src/ng/parse.js`: 160 | 161 | --- 162 | 163 | Angular expressions are generally considered safe because these expressions only have direct 164 | access to \$scope and locals. However, one can obtain the ability to execute arbitrary JS code by 165 | obtaining a reference to native JS functions such as the Function constructor. 166 | 167 | As an example, consider the following Angular expression: 168 | 169 | ```javascript 170 | {}.toString.constructor(alert("evil JS code")) 171 | ``` 172 | 173 | We want to prevent this type of access. For the sake of performance, during the lexing phase we 174 | disallow any "dotted" access to any member named "constructor". 175 | 176 | For reflective calls (a[b]) we check that the value of the lookup is not the Function constructor 177 | while evaluating the expression, which is a stronger but more expensive test. Since reflective 178 | calls are expensive anyway, this is not such a big deal compared to static dereferencing. 179 | This sandboxing technique is not perfect and doesn't aim to be. The goal is to prevent exploits 180 | against the expression language, but not to prevent exploits that were enabled by exposing 181 | sensitive JavaScript or browser apis on Scope. Exposing such objects on a Scope is never a good 182 | practice and therefore we are not even trying to protect against interaction with an object 183 | explicitly exposed in this way. 184 | 185 | A developer could foil the name check by aliasing the Function constructor under a different 186 | name on the scope. 187 | 188 | In general, it is not possible to access a Window object from an angular expression unless a 189 | window or some DOM object that has a reference to window is published onto a Scope. 190 | 191 | --- 192 | 193 |
194 | 195 | ## Authorship 196 | 197 | Kudos go entirely to the great angular.js team, it's their implementation! 198 | 199 |
200 | 201 | ## Contributing 202 | 203 | Suggestions and bug-fixes are always appreciated. Don't hesitate to create an issue or pull-request. All contributed code should pass 204 | 205 | 1. the tests in node.js by running `npm test` 206 | 2. the tests in all major browsers by running `npm run test-browser` and then visiting `http://localhost:8080/bundle` 207 | 208 |
209 | 210 | ## License 211 | 212 | [Unlicense](http://unlicense.org/) 213 | 214 | ## Sponsors 215 | 216 | [](https://peerigon.com) 217 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import globals from "globals"; 2 | 3 | export default { 4 | ignores: ["node_modules"], 5 | languageOptions: { 6 | globals: { 7 | ...globals.node, 8 | ...globals.mocha, 9 | }, 10 | }, 11 | rules: { 12 | "accessor-pairs": 2, 13 | "array-bracket-spacing": [2, "never"], 14 | "arrow-parens": 0, 15 | "arrow-spacing": [2, { before: true, after: true }], 16 | "block-scoped-var": 2, 17 | "block-spacing": [2, "always"], 18 | "brace-style": 0, 19 | "callback-return": 2, 20 | camelcase: [0, { properties: "never" }], 21 | "comma-dangle": [0, "always-multiline"], 22 | "comma-spacing": [2, { before: false, after: true }], 23 | "comma-style": [2, "last"], 24 | complexity: [2, 10], 25 | "computed-property-spacing": [2, "never"], 26 | "consistent-return": 0, 27 | "consistent-this": [2, "self"], 28 | "constructor-super": 2, 29 | curly: [2, "all"], 30 | "default-case": 0, 31 | "dot-location": [2, "property"], 32 | "dot-notation": 2, 33 | "eol-last": 2, 34 | eqeqeq: [2, "smart"], 35 | "func-names": 0, 36 | "func-style": [2, "declaration"], 37 | "generator-star-spacing": [2, { before: false, after: true }], 38 | "global-require": 0, 39 | "guard-for-in": 2, 40 | "handle-callback-err": 2, 41 | "id-length": 0, 42 | "id-match": 0, 43 | "init-declarations": 0, 44 | "key-spacing": [ 45 | 2, 46 | { beforeColon: false, afterColon: true, mode: "strict" }, 47 | ], 48 | "linebreak-style": 0, 49 | "lines-around-comment": 0, 50 | "max-nested-callbacks": 0, 51 | "new-cap": [ 52 | 2, 53 | { 54 | newIsCapExceptions: [ 55 | "Boom.badRequest", 56 | "Boom.forbidden", 57 | "Boom.unauthorized", 58 | "Boom.wrap", 59 | ], 60 | capIsNewExceptions: ["squeeze.Squeeze"], 61 | }, 62 | ], 63 | "new-parens": 2, 64 | "newline-after-var": 0, 65 | "no-alert": 2, 66 | "no-array-constructor": 2, 67 | "no-caller": 2, 68 | "no-catch-shadow": 0, 69 | "no-class-assign": 2, 70 | "no-console": 2, 71 | "no-const-assign": 2, 72 | "no-constant-condition": [2, { checkLoops: false }], 73 | "no-continue": 0, 74 | "no-control-regex": 0, 75 | "no-debugger": 2, 76 | "no-delete-var": 2, 77 | "no-div-regex": 2, 78 | "no-dupe-args": 2, 79 | "no-dupe-class-members": 2, 80 | "no-dupe-keys": 2, 81 | "no-duplicate-case": 2, 82 | "no-else-return": 2, 83 | "no-empty": 2, 84 | "no-empty-character-class": 2, 85 | "no-eval": 2, 86 | "no-ex-assign": 2, 87 | "no-extend-native": 2, 88 | "no-extra-bind": 2, 89 | "no-extra-boolean-cast": 2, 90 | "no-extra-parens": [2, "functions"], 91 | "no-extra-semi": 2, 92 | "no-fallthrough": 2, 93 | "no-floating-decimal": 2, 94 | "no-func-assign": 2, 95 | "no-implicit-coercion": 0, 96 | "no-implied-eval": 2, 97 | "no-inline-comments": 0, 98 | "no-inner-declarations": 2, 99 | "no-invalid-regexp": 2, 100 | "no-irregular-whitespace": 2, 101 | "no-iterator": 2, 102 | "no-label-var": 2, 103 | "no-labels": 0, 104 | "no-lone-blocks": 2, 105 | "no-lonely-if": 2, 106 | "no-loop-func": 2, 107 | "no-mixed-requires": 2, 108 | "no-mixed-spaces-and-tabs": 0, 109 | "no-multi-spaces": 2, 110 | "no-multi-str": 2, 111 | "no-multiple-empty-lines": [2, { max: 1 }], 112 | "no-native-reassign": 2, 113 | "no-negated-in-lhs": 2, 114 | "no-new": 2, 115 | "no-new-func": 2, 116 | "no-new-object": 2, 117 | "no-new-require": 2, 118 | "no-new-wrappers": 2, 119 | "no-obj-calls": 2, 120 | "no-octal": 2, 121 | "no-octal-escape": 2, 122 | "no-param-reassign": 0, 123 | "no-path-concat": 2, 124 | "no-process-env": 2, 125 | "no-process-exit": 2, 126 | "no-proto": 2, 127 | "no-redeclare": 2, 128 | "no-regex-spaces": 2, 129 | "no-restricted-modules": 0, 130 | "no-restricted-syntax": 0, 131 | "no-return-assign": 0, 132 | "no-script-url": 2, 133 | "no-self-compare": 2, 134 | "no-sequences": 2, 135 | "no-shadow": 0, 136 | "no-shadow-restricted-names": 2, 137 | "no-spaced-func": 2, 138 | "no-sparse-arrays": 2, 139 | "no-ternary": 0, 140 | "no-this-before-super": 2, 141 | "no-throw-literal": 2, 142 | "no-trailing-spaces": 2, 143 | "no-undef": 2, 144 | "no-undef-init": 0, 145 | "no-undefined": 0, 146 | "no-underscore-dangle": 0, 147 | "no-unexpected-multiline": 2, 148 | "no-unneeded-ternary": 2, 149 | "no-unreachable": 2, 150 | "no-unused-expressions": 2, 151 | "no-unused-vars": 2, 152 | "no-use-before-define": [2, "nofunc"], 153 | "no-useless-call": 2, 154 | "no-useless-concat": 2, 155 | "no-var": 0, 156 | "no-void": 2, 157 | "no-warning-comments": [ 158 | 2, 159 | { terms: ["todo", "fixme"], location: "anywhere" }, 160 | ], 161 | "no-with": 2, 162 | "object-curly-spacing": 0, 163 | "object-shorthand": 0, 164 | "one-var": 0, 165 | "operator-assignment": [0, "always"], 166 | "operator-linebreak": 0, 167 | "padded-blocks": [2, "never"], 168 | "prefer-arrow-callback": 0, 169 | "prefer-const": 0, 170 | "prefer-destructuring": [ 171 | 0, 172 | { 173 | AssignmentExpression: { 174 | array: true, 175 | object: false, 176 | }, 177 | }, 178 | ], 179 | "prefer-reflect": 0, 180 | "prefer-spread": 0, 181 | "prefer-template": 0, 182 | quotes: [2, "double", { avoidEscape: true }], 183 | "quote-props": [2, "as-needed"], 184 | radix: 2, 185 | "require-jsdoc": 0, 186 | "require-yield": 2, 187 | semi: [2, "always"], 188 | "semi-spacing": [2, { before: false, after: true }], 189 | "sort-vars": 0, 190 | "use-isnan": 2, 191 | "valid-jsdoc": 0, 192 | "valid-typeof": 2, 193 | "vars-on-top": 0, 194 | "wrap-iife": [2, "inside"], 195 | "wrap-regex": 0, 196 | yoda: [2, "never"], 197 | }, 198 | }; 199 | -------------------------------------------------------------------------------- /lib/main.d.ts: -------------------------------------------------------------------------------- 1 | interface LexerOptions { 2 | isIdentifierStart?: (char: string) => boolean; 3 | isIdentifierContinue?: (char: string) => boolean; 4 | } 5 | 6 | interface ParserOptions { 7 | csp?: boolean; 8 | literals?: { 9 | [x: string]: any; 10 | }; 11 | } 12 | 13 | interface Filters { 14 | [x: string]: FilterFunction; 15 | } 16 | 17 | interface Cache { 18 | [x: string]: any; 19 | } 20 | 21 | interface CompileFuncOptions extends LexerOptions { 22 | filters?: Filters; 23 | cache?: Cache; 24 | } 25 | 26 | type EvaluatorFunc = { 27 | (scope?: any, context?: any): any; 28 | ast: any; 29 | assign: (scope: any, value: any) => any; 30 | }; 31 | 32 | type CompileFunc = { 33 | (tag: string, options?: CompileFuncOptions): EvaluatorFunc; 34 | cache: Cache; 35 | }; 36 | 37 | type FilterFunction = (input: any, ...args: any[]) => any; 38 | 39 | export const compile: CompileFunc; 40 | 41 | export class Lexer { 42 | constructor(options?: LexerOptions); 43 | } 44 | 45 | export const filters: Filters; 46 | 47 | export class Parser { 48 | constructor( 49 | lexer: Lexer, 50 | filterFunction: (tag: any) => FilterFunction, 51 | options?: ParserOptions 52 | ); 53 | } 54 | -------------------------------------------------------------------------------- /lib/main.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var parse = require("./parse.js"); 4 | 5 | var filters = {}; 6 | var Lexer = parse.Lexer; 7 | var Parser = parse.Parser; 8 | 9 | function addOptionDefaults(options) { 10 | options = options || {}; 11 | if (options.filters) { 12 | options.cache = options.cache || {}; 13 | } 14 | options.cache = options.cache || compile.cache; 15 | options.filters = options.filters || filters; 16 | return options; 17 | } 18 | 19 | function getParserOptions(options) { 20 | return { 21 | handleThis: options.handleThis != null ? options.handleThis : true, 22 | csp: options.csp != null ? options.csp : false, // noUnsafeEval, 23 | literals: 24 | options.literals != null 25 | ? options.literals 26 | : { 27 | // defined at: function $ParseProvider() { 28 | true: true, 29 | false: false, 30 | null: null, 31 | /*eslint no-undefined: 0*/ 32 | undefined: undefined, 33 | /* eslint: no-undefined: 1 */ 34 | }, 35 | }; 36 | } 37 | 38 | /** 39 | * Compiles src and returns a function that executes src on a target object. 40 | * To speed up further calls the compiled function is cached under compile.cache[src] if options.filters is not present. 41 | * 42 | * @param {string} src 43 | * @param {object | undefined} options 44 | * @returns {function} 45 | */ 46 | function compile(src, options) { 47 | if (typeof src !== "string") { 48 | throw new TypeError( 49 | "src must be a string, instead saw '" + typeof src + "'" 50 | ); 51 | } 52 | options = addOptionDefaults(options); 53 | var lexerOptions = options; 54 | var parserOptions = getParserOptions(options); 55 | 56 | var lexer = new Lexer(lexerOptions); 57 | var parser = new Parser( 58 | lexer, 59 | function getFilter(name) { 60 | return options.filters[name]; 61 | }, 62 | parserOptions 63 | ); 64 | 65 | if (!options.cache) { 66 | return parser.parse(src); 67 | } 68 | delete options.src; 69 | var cacheKey = JSON.stringify(Object.assign({ src: src }, options)); 70 | 71 | var cached = options.cache[cacheKey]; 72 | if (!cached) { 73 | cached = options.cache[cacheKey] = parser.parse(src); 74 | } 75 | return cached; 76 | } 77 | 78 | /** 79 | * A cache containing all compiled functions. The src is used as key. 80 | * Set this on false to disable the cache. 81 | * 82 | * @type {object} 83 | */ 84 | compile.cache = Object.create(null); 85 | 86 | exports.Lexer = Lexer; 87 | exports.Parser = Parser; 88 | exports.compile = compile; 89 | exports.filters = filters; 90 | -------------------------------------------------------------------------------- /lib/main.test-d.ts: -------------------------------------------------------------------------------- 1 | import expressions from "./main.js"; 2 | import { filters } from "./main.js"; 3 | 4 | const { Parser, Lexer } = expressions; 5 | 6 | function getFilters(tag: any) { 7 | return filters[tag]; 8 | } 9 | 10 | const ppp = new Parser(new Lexer(), getFilters, { csp: true }); 11 | const f = expressions.compile("x + 1"); 12 | const myResult = f.assign({}, 123); 13 | 14 | const result = f({ x: 4 }); 15 | const result2 = f({ x: 4 }, { y: 3 }); 16 | 17 | function validChars(ch: string) { 18 | return ( 19 | (ch >= "a" && ch <= "z") || 20 | (ch >= "A" && ch <= "Z") || 21 | ch === "_" || 22 | ch === "$" || 23 | "ÀÈÌÒÙàèìòùÁÉÍÓÚáéíóúÂÊÎÔÛâêîôûÃÑÕãñõÄËÏÖÜŸäëïöüÿß".indexOf(ch) !== -1 24 | ); 25 | } 26 | 27 | expressions.compile("être_embarassé", { isIdentifierStart: validChars }); 28 | 29 | const cache = expressions.compile.cache; 30 | const ast = expressions.compile("foobar").ast; 31 | 32 | filters.uppercase = (input: string): string => input.toUpperCase(); 33 | 34 | expressions.compile("number | square", { 35 | filters: { 36 | square: (input: number) => input * input, 37 | }, 38 | cache: {}, 39 | }); 40 | -------------------------------------------------------------------------------- /lib/parse.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /* eslint complexity: 0*/ 4 | /* eslint eqeqeq: 0*/ 5 | /* eslint func-style: 0*/ 6 | /* eslint no-warning-comments: 0*/ 7 | 8 | var window = { document: {} }; 9 | 10 | var hasOwnProperty = Object.prototype.hasOwnProperty; 11 | 12 | var lowercase = function (string) { 13 | return isString(string) ? string.toLowerCase() : string; 14 | }; 15 | 16 | /** 17 | * @ngdoc function 18 | * @name angular.isArray 19 | * @module ng 20 | * @kind function 21 | * 22 | * @description 23 | * Determines if a reference is an `Array`. 24 | * 25 | * @param {*} value Reference to check. 26 | * @returns {boolean} True if `value` is an `Array`. 27 | */ 28 | var isArray = Array.isArray; 29 | 30 | var manualLowercase = function (s) { 31 | return isString(s) 32 | ? s.replace(/[A-Z]/g, function (ch) { 33 | return String.fromCharCode(ch.charCodeAt(0) | 32); 34 | }) 35 | : s; 36 | }; 37 | 38 | // String#toLowerCase and String#toUpperCase don't produce correct results in browsers with Turkish 39 | // locale, for this reason we need to detect this case and redefine lowercase/uppercase methods 40 | // with correct but slower alternatives. See https://github.com/angular/angular.js/issues/11387 41 | if ("I".toLowerCase() !== "i") { 42 | lowercase = manualLowercase; 43 | } 44 | 45 | // Run a function and disallow temporarly the use of the Function constructor 46 | // This makes arbitrary code generation attacks way more complicated. 47 | function runWithFunctionConstructorProtection(fn) { 48 | var originalFunctionConstructor = Function.prototype.constructor; 49 | delete Function.prototype.constructor; 50 | var result = fn(); 51 | // eslint-disable-next-line no-extend-native 52 | Function.prototype.constructor = originalFunctionConstructor; 53 | return result; 54 | } 55 | 56 | var jqLite, // delay binding since jQuery could be loaded after us. 57 | toString = Object.prototype.toString, 58 | getPrototypeOf = Object.getPrototypeOf, 59 | ngMinErr = minErr("ng"); 60 | 61 | /** 62 | * @private 63 | * @param {*} obj 64 | * @return {boolean} Returns true if `obj` is an array or array-like object (NodeList, Arguments, 65 | * String ...) 66 | */ 67 | function isArrayLike(obj) { 68 | // `null`, `undefined` and `window` are not array-like 69 | if (obj == null || isWindow(obj)) { 70 | return false; 71 | } 72 | 73 | // arrays, strings and jQuery/jqLite objects are array like 74 | // * jqLite is either the jQuery or jqLite constructor function 75 | // * we have to check the existence of jqLite first as this method is called 76 | // via the forEach method when constructing the jqLite object in the first place 77 | if (isArray(obj) || isString(obj) || (jqLite && obj instanceof jqLite)) { 78 | return true; 79 | } 80 | 81 | // Support: iOS 8.2 (not reproducible in simulator) 82 | // "length" in obj used to prevent JIT error (gh-11508) 83 | var length = "length" in Object(obj) && obj.length; 84 | 85 | // NodeList objects (with `item` method) and 86 | // other objects with suitable length characteristics are array-like 87 | return ( 88 | isNumber(length) && 89 | ((length >= 0 && (length - 1 in obj || obj instanceof Array)) || 90 | typeof obj.item === "function") 91 | ); 92 | } 93 | 94 | /** 95 | * @ngdoc function 96 | * @name angular.forEach 97 | * @module ng 98 | * @kind function 99 | * 100 | * @description 101 | * Invokes the `iterator` function once for each item in `obj` collection, which can be either an 102 | * object or an array. The `iterator` function is invoked with `iterator(value, key, obj)`, where `value` 103 | * is the value of an object property or an array element, `key` is the object property key or 104 | * array element index and obj is the `obj` itself. Specifying a `context` for the function is optional. 105 | * 106 | * It is worth noting that `.forEach` does not iterate over inherited properties because it filters 107 | * using the `hasOwnProperty` method. 108 | * 109 | * Unlike ES262's 110 | * [Array.prototype.forEach](http://www.ecma-international.org/ecma-262/5.1/#sec-15.4.4.18), 111 | * providing 'undefined' or 'null' values for `obj` will not throw a TypeError, but rather just 112 | * return the value provided. 113 | * 114 | ```js 115 | var values = {name: 'misko', gender: 'male'}; 116 | var log = []; 117 | angular.forEach(values, function(value, key) { 118 | this.push(key + ': ' + value); 119 | }, log); 120 | expect(log).toEqual(['name: misko', 'gender: male']); 121 | ``` 122 | * 123 | * @param {Object|Array} obj Object to iterate over. 124 | * @param {Function} iterator Iterator function. 125 | * @param {Object=} context Object to become context (`this`) for the iterator function. 126 | * @returns {Object|Array} Reference to `obj`. 127 | */ 128 | 129 | function forEach(obj, iterator, context) { 130 | var key, length; 131 | if (obj) { 132 | if (isFunction(obj)) { 133 | for (key in obj) { 134 | if ( 135 | key !== "prototype" && 136 | key !== "length" && 137 | key !== "name" && 138 | obj.hasOwnProperty(key) 139 | ) { 140 | iterator.call(context, obj[key], key, obj); 141 | } 142 | } 143 | } else if (isArray(obj) || isArrayLike(obj)) { 144 | var isPrimitive = typeof obj !== "object"; 145 | for (key = 0, length = obj.length; key < length; key++) { 146 | if (isPrimitive || key in obj) { 147 | iterator.call(context, obj[key], key, obj); 148 | } 149 | } 150 | } else if (obj.forEach && obj.forEach !== forEach) { 151 | obj.forEach(iterator, context, obj); 152 | } else if (isBlankObject(obj)) { 153 | // createMap() fast path --- Safe to avoid hasOwnProperty check because prototype chain is empty 154 | // eslint-disable-next-line guard-for-in 155 | for (key in obj) { 156 | iterator.call(context, obj[key], key, obj); 157 | } 158 | } else if (typeof obj.hasOwnProperty === "function") { 159 | // Slow path for objects inheriting Object.prototype, hasOwnProperty check needed 160 | for (key in obj) { 161 | if (obj.hasOwnProperty(key)) { 162 | iterator.call(context, obj[key], key, obj); 163 | } 164 | } 165 | } else { 166 | // Slow path for objects which do not have a method `hasOwnProperty` 167 | for (key in obj) { 168 | if (hasOwnProperty.call(obj, key)) { 169 | iterator.call(context, obj[key], key, obj); 170 | } 171 | } 172 | } 173 | } 174 | return obj; 175 | } 176 | 177 | /** 178 | * Set or clear the hashkey for an object. 179 | * @param obj object 180 | * @param h the hashkey (!truthy to delete the hashkey) 181 | */ 182 | function setHashKey(obj, h) { 183 | if (h) { 184 | obj.$$hashKey = h; 185 | } else { 186 | delete obj.$$hashKey; 187 | } 188 | } 189 | 190 | function noop() {} 191 | 192 | /** 193 | * @ngdoc function 194 | * @name angular.isUndefined 195 | * @module ng 196 | * @kind function 197 | * 198 | * @description 199 | * Determines if a reference is undefined. 200 | * 201 | * @param {*} value Reference to check. 202 | * @returns {boolean} True if `value` is undefined. 203 | */ 204 | function isUndefined(value) { 205 | return typeof value === "undefined"; 206 | } 207 | 208 | /** 209 | * @ngdoc function 210 | * @name angular.isDefined 211 | * @module ng 212 | * @kind function 213 | * 214 | * @description 215 | * Determines if a reference is defined. 216 | * 217 | * @param {*} value Reference to check. 218 | * @returns {boolean} True if `value` is defined. 219 | */ 220 | function isDefined(value) { 221 | return typeof value !== "undefined"; 222 | } 223 | 224 | /** 225 | * @ngdoc function 226 | * @name angular.isObject 227 | * @module ng 228 | * @kind function 229 | * 230 | * @description 231 | * Determines if a reference is an `Object`. Unlike `typeof` in JavaScript, `null`s are not 232 | * considered to be objects. Note that JavaScript arrays are objects. 233 | * 234 | * @param {*} value Reference to check. 235 | * @returns {boolean} True if `value` is an `Object` but not `null`. 236 | */ 237 | function isObject(value) { 238 | // http://jsperf.com/isobject4 239 | return value !== null && typeof value === "object"; 240 | } 241 | 242 | /** 243 | * Determine if a value is an object with a null prototype 244 | * 245 | * @returns {boolean} True if `value` is an `Object` with a null prototype 246 | */ 247 | function isBlankObject(value) { 248 | return value !== null && typeof value === "object" && !getPrototypeOf(value); 249 | } 250 | 251 | /** 252 | * @ngdoc function 253 | * @name angular.isString 254 | * @module ng 255 | * @kind function 256 | * 257 | * @description 258 | * Determines if a reference is a `String`. 259 | * 260 | * @param {*} value Reference to check. 261 | * @returns {boolean} True if `value` is a `String`. 262 | */ 263 | function isString(value) { 264 | return typeof value === "string"; 265 | } 266 | 267 | /** 268 | * @ngdoc function 269 | * @name angular.isNumber 270 | * @module ng 271 | * @kind function 272 | * 273 | * @description 274 | * Determines if a reference is a `Number`. 275 | * 276 | * This includes the "special" numbers `NaN`, `+Infinity` and `-Infinity`. 277 | * 278 | * If you wish to exclude these then you can use the native 279 | * [`isFinite'](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/isFinite) 280 | * method. 281 | * 282 | * @param {*} value Reference to check. 283 | * @returns {boolean} True if `value` is a `Number`. 284 | */ 285 | function isNumber(value) { 286 | return typeof value === "number"; 287 | } 288 | 289 | /** 290 | * @ngdoc function 291 | * @name angular.isFunction 292 | * @module ng 293 | * @kind function 294 | * 295 | * @description 296 | * Determines if a reference is a `Function`. 297 | * 298 | * @param {*} value Reference to check. 299 | * @returns {boolean} True if `value` is a `Function`. 300 | */ 301 | function isFunction(value) { 302 | return typeof value === "function"; 303 | } 304 | 305 | /** 306 | * Checks if `obj` is a window object. 307 | * 308 | * @private 309 | * @param {*} obj Object to check 310 | * @returns {boolean} True if `obj` is a window obj. 311 | */ 312 | function isWindow(obj) { 313 | return obj && obj.window === obj; 314 | } 315 | 316 | function isScope(obj) { 317 | return obj && obj.$evalAsync && obj.$watch; 318 | } 319 | 320 | var TYPED_ARRAY_REGEXP = 321 | /^\[object (?:Uint8|Uint8Clamped|Uint16|Uint32|Int8|Int16|Int32|Float32|Float64)Array\]$/; 322 | function isTypedArray(value) { 323 | return ( 324 | value && 325 | isNumber(value.length) && 326 | TYPED_ARRAY_REGEXP.test(toString.call(value)) 327 | ); 328 | } 329 | 330 | function isArrayBuffer(obj) { 331 | return toString.call(obj) === "[object ArrayBuffer]"; 332 | } 333 | /** 334 | * @ngdoc function 335 | * @name angular.copy 336 | * @module ng 337 | * @kind function 338 | * 339 | * @description 340 | * Creates a deep copy of `source`, which should be an object or an array. 341 | * 342 | * * If no destination is supplied, a copy of the object or array is created. 343 | * * If a destination is provided, all of its elements (for arrays) or properties (for objects) 344 | * are deleted and then all elements/properties from the source are copied to it. 345 | * * If `source` is not an object or array (inc. `null` and `undefined`), `source` is returned. 346 | * * If `source` is identical to `destination` an exception will be thrown. 347 | * 348 | *
349 | *
350 | * Only enumerable properties are taken into account. Non-enumerable properties (both on `source` 351 | * and on `destination`) will be ignored. 352 | *
353 | * 354 | * @param {*} source The source that will be used to make a copy. 355 | * Can be any type, including primitives, `null`, and `undefined`. 356 | * @param {(Object|Array)=} destination Destination into which the source is copied. If 357 | * provided, must be of the same type as `source`. 358 | * @returns {*} The copy or updated `destination`, if `destination` was specified. 359 | * 360 | * @example 361 | 362 | 363 |
364 |
365 |
366 |
367 | Gender: 368 |
369 | 370 | 371 |
372 |
form = {{user | json}}
373 |
master = {{master | json}}
374 |
375 |
376 | 377 | // Module: copyExample 378 | angular. 379 | module('copyExample', []). 380 | controller('ExampleController', ['$scope', function($scope) { 381 | $scope.master = {}; 382 | 383 | $scope.reset = function() { 384 | // Example with 1 argument 385 | $scope.user = angular.copy($scope.master); 386 | }; 387 | 388 | $scope.update = function(user) { 389 | // Example with 2 arguments 390 | angular.copy(user, $scope.master); 391 | }; 392 | 393 | $scope.reset(); 394 | }]); 395 | 396 |
397 | */ 398 | function copy(source, destination) { 399 | var stackSource = []; 400 | var stackDest = []; 401 | 402 | if (destination) { 403 | if (isTypedArray(destination) || isArrayBuffer(destination)) { 404 | throw ngMinErr( 405 | "cpta", 406 | "Can't copy! TypedArray destination cannot be mutated." 407 | ); 408 | } 409 | if (source === destination) { 410 | throw ngMinErr( 411 | "cpi", 412 | "Can't copy! Source and destination are identical." 413 | ); 414 | } 415 | 416 | // Empty the destination object 417 | if (isArray(destination)) { 418 | destination.length = 0; 419 | } else { 420 | forEach(destination, function (value, key) { 421 | if (key !== "$$hashKey") { 422 | delete destination[key]; 423 | } 424 | }); 425 | } 426 | 427 | stackSource.push(source); 428 | stackDest.push(destination); 429 | return copyRecurse(source, destination); 430 | } 431 | 432 | return copyElement(source); 433 | 434 | function copyRecurse(source, destination) { 435 | var h = destination.$$hashKey; 436 | var key; 437 | if (isArray(source)) { 438 | for (var i = 0, ii = source.length; i < ii; i++) { 439 | destination.push(copyElement(source[i])); 440 | } 441 | } else if (isBlankObject(source)) { 442 | // createMap() fast path --- Safe to avoid hasOwnProperty check because prototype chain is empty 443 | // eslint-disable-next-line guard-for-in 444 | for (key in source) { 445 | destination[key] = copyElement(source[key]); 446 | } 447 | } else if (source && typeof source.hasOwnProperty === "function") { 448 | // Slow path, which must rely on hasOwnProperty 449 | for (key in source) { 450 | if (source.hasOwnProperty(key)) { 451 | destination[key] = copyElement(source[key]); 452 | } 453 | } 454 | } else { 455 | // Slowest path --- hasOwnProperty can't be called as a method 456 | for (key in source) { 457 | if (hasOwnProperty.call(source, key)) { 458 | destination[key] = copyElement(source[key]); 459 | } 460 | } 461 | } 462 | setHashKey(destination, h); 463 | return destination; 464 | } 465 | 466 | function copyElement(source) { 467 | // Simple values 468 | if (!isObject(source)) { 469 | return source; 470 | } 471 | 472 | // Already copied values 473 | var index = stackSource.indexOf(source); 474 | if (index !== -1) { 475 | return stackDest[index]; 476 | } 477 | 478 | if (isWindow(source) || isScope(source)) { 479 | throw ngMinErr( 480 | "cpws", 481 | "Can't copy! Making copies of Window or Scope instances is not supported." 482 | ); 483 | } 484 | 485 | var needsRecurse = false; 486 | var destination = copyType(source); 487 | 488 | if (destination === undefined) { 489 | destination = isArray(source) 490 | ? [] 491 | : Object.create(getPrototypeOf(source)); 492 | needsRecurse = true; 493 | } 494 | 495 | stackSource.push(source); 496 | stackDest.push(destination); 497 | 498 | return needsRecurse ? copyRecurse(source, destination) : destination; 499 | } 500 | 501 | function copyType(source) { 502 | switch (toString.call(source)) { 503 | case "[object Int8Array]": 504 | case "[object Int16Array]": 505 | case "[object Int32Array]": 506 | case "[object Float32Array]": 507 | case "[object Float64Array]": 508 | case "[object Uint8Array]": 509 | case "[object Uint8ClampedArray]": 510 | case "[object Uint16Array]": 511 | case "[object Uint32Array]": 512 | return new source.constructor( 513 | copyElement(source.buffer), 514 | source.byteOffset, 515 | source.length 516 | ); 517 | 518 | case "[object ArrayBuffer]": 519 | // Support: IE10 520 | if (!source.slice) { 521 | // If we're in this case we know the environment supports ArrayBuffer 522 | 523 | var copied = new ArrayBuffer(source.byteLength); 524 | new Uint8Array(copied).set(new Uint8Array(source)); 525 | 526 | return copied; 527 | } 528 | return source.slice(0); 529 | 530 | case "[object Boolean]": 531 | case "[object Number]": 532 | case "[object String]": 533 | case "[object Date]": 534 | return new source.constructor(source.valueOf()); 535 | 536 | case "[object RegExp]": 537 | var re = new RegExp( 538 | source.source, 539 | source.toString().match(/[^\/]*$/)[0] 540 | ); 541 | re.lastIndex = source.lastIndex; 542 | return re; 543 | 544 | case "[object Blob]": 545 | return new source.constructor([source], { type: source.type }); 546 | } 547 | 548 | if (isFunction(source.cloneNode)) { 549 | return source.cloneNode(true); 550 | } 551 | } 552 | } 553 | 554 | /** 555 | * @ngdoc function 556 | * @name angular.bind 557 | * @module ng 558 | * @kind function 559 | * 560 | * @description 561 | * Returns a function which calls function `fn` bound to `self` (`self` becomes the `this` for 562 | * `fn`). You can supply optional `args` that are prebound to the function. This feature is also 563 | * known as [partial application](http://en.wikipedia.org/wiki/Partial_application), as 564 | * distinguished from [function currying](http://en.wikipedia.org/wiki/Currying#Contrast_with_partial_function_application). 565 | * 566 | * @param {Object} self Context which `fn` should be evaluated in. 567 | * @param {function()} fn Function to be bound. 568 | * @param {...*} args Optional arguments to be prebound to the `fn` function call. 569 | * @returns {function()} Function that wraps the `fn` with all the specified bindings. 570 | */ 571 | 572 | function toJsonReplacer(key, value) { 573 | var val = value; 574 | 575 | if ( 576 | typeof key === "string" && 577 | key.charAt(0) === "$" && 578 | key.charAt(1) === "$" 579 | ) { 580 | val = undefined; 581 | } else if (isWindow(value)) { 582 | val = "$WINDOW"; 583 | } else if (value && window.document === value) { 584 | val = "$DOCUMENT"; 585 | } else if (isScope(value)) { 586 | val = "$SCOPE"; 587 | } 588 | 589 | return val; 590 | } 591 | 592 | /** 593 | * Creates a new object without a prototype. This object is useful for lookup without having to 594 | * guard against prototypically inherited properties via hasOwnProperty. 595 | * 596 | * Related micro-benchmarks: 597 | * - http://jsperf.com/object-create2 598 | * - http://jsperf.com/proto-map-lookup/2 599 | * - http://jsperf.com/for-in-vs-object-keys2 600 | * 601 | * @returns {Object} 602 | */ 603 | function createMap() { 604 | return Object.create(null); 605 | } 606 | 607 | function serializeObject(obj) { 608 | var seen = []; 609 | 610 | return JSON.stringify(obj, function (key, val) { 611 | val = toJsonReplacer(key, val); 612 | if (isObject(val)) { 613 | if (seen.indexOf(val) >= 0) { 614 | return "..."; 615 | } 616 | 617 | seen.push(val); 618 | } 619 | return val; 620 | }); 621 | } 622 | 623 | function toDebugString(obj) { 624 | if (typeof obj === "function") { 625 | return obj.toString().replace(/ \{[\s\S]*$/, ""); 626 | } else if (isUndefined(obj)) { 627 | return "undefined"; 628 | } else if (typeof obj !== "string") { 629 | return serializeObject(obj); 630 | } 631 | return obj; 632 | } 633 | 634 | /** 635 | * @description 636 | * 637 | * This object provides a utility for producing rich Error messages within 638 | * Angular. It can be called as follows: 639 | * 640 | * var exampleMinErr = minErr('example'); 641 | * throw exampleMinErr('one', 'This {0} is {1}', foo, bar); 642 | * 643 | * The above creates an instance of minErr in the example namespace. The 644 | * resulting error will have a namespaced error code of example.one. The 645 | * resulting error will replace {0} with the value of foo, and {1} with the 646 | * value of bar. The object is not restricted in the number of arguments it can 647 | * take. 648 | * 649 | * If fewer arguments are specified than necessary for interpolation, the extra 650 | * interpolation markers will be preserved in the final string. 651 | * 652 | * Since data will be parsed statically during a build step, some restrictions 653 | * are applied with respect to how minErr instances are created and called. 654 | * Instances should have names of the form namespaceMinErr for a minErr created 655 | * using minErr('namespace') . Error codes, namespaces and template strings 656 | * should all be static strings, not variables or general expressions. 657 | * 658 | * @param {string} module The namespace to use for the new minErr instance. 659 | * @param {function} ErrorConstructor Custom error constructor to be instantiated when returning 660 | * error from returned function, for cases when a particular type of error is useful. 661 | * @returns {function(code:string, template:string, ...templateArgs): Error} minErr instance 662 | */ 663 | 664 | function minErr(module, ErrorConstructor) { 665 | ErrorConstructor = ErrorConstructor || Error; 666 | return function () { 667 | var SKIP_INDEXES = 2; 668 | 669 | var templateArgs = arguments, 670 | code = templateArgs[0], 671 | message = "[" + (module ? module + ":" : "") + code + "] ", 672 | template = templateArgs[1], 673 | paramPrefix, 674 | i; 675 | 676 | message += template.replace(/\{\d+\}/g, function (match) { 677 | var index = +match.slice(1, -1), 678 | shiftedIndex = index + SKIP_INDEXES; 679 | 680 | if (shiftedIndex < templateArgs.length) { 681 | return toDebugString(templateArgs[shiftedIndex]); 682 | } 683 | 684 | return match; 685 | }); 686 | 687 | message += 688 | '\nhttp://errors.angularjs.org/"NG_VERSION_FULL"/' + 689 | (module ? module + "/" : "") + 690 | code; 691 | 692 | for ( 693 | i = SKIP_INDEXES, paramPrefix = "?"; 694 | i < templateArgs.length; 695 | i++, paramPrefix = "&" 696 | ) { 697 | message += 698 | paramPrefix + 699 | "p" + 700 | (i - SKIP_INDEXES) + 701 | "=" + 702 | encodeURIComponent(toDebugString(templateArgs[i])); 703 | } 704 | 705 | return new ErrorConstructor(message); 706 | }; 707 | } 708 | 709 | /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * 710 | * Any commits to this file should be reviewed with security in mind. * 711 | * Changes to this file can potentially create security vulnerabilities. * 712 | * An approval from 2 Core members with history of modifying * 713 | * this file is required. * 714 | * * 715 | * Does the change somehow allow for arbitrary javascript to be executed? * 716 | * Or allows for someone to change the prototype of built-in objects? * 717 | * Or gives undesired access to variables likes document or window? * 718 | * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ 719 | 720 | var $parseMinErr = minErr("$parse"); 721 | 722 | // Sandboxing Angular Expressions 723 | // ------------------------------ 724 | // Angular expressions are no longer sandboxed. So it is now even easier to access arbitrary JS code by 725 | // various means such as obtaining a reference to native JS functions like the Function constructor. 726 | // 727 | // As an example, consider the following Angular expression: 728 | // 729 | // {}.toString.constructor('alert("evil JS code")') 730 | // 731 | // It is important to realize that if you create an expression from a string that contains user provided 732 | // content then it is possible that your application contains a security vulnerability to an XSS style attack. 733 | // 734 | // See https://docs.angularjs.org/guide/security 735 | 736 | function getStringValue(name) { 737 | // Property names must be strings. This means that non-string objects cannot be used 738 | // as keys in an object. Any non-string object, including a number, is typecasted 739 | // into a string via the toString method. 740 | // -- MDN, https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Operators/Property_accessors#Property_names 741 | // 742 | // So, to ensure that we are checking the same `name` that JavaScript would use, we cast it 743 | // to a string. It's not always possible. If `name` is an object and its `toString` method is 744 | // 'broken' (doesn't return a string, isn't a function, etc.), an error will be thrown: 745 | // 746 | // TypeError: Cannot convert object to primitive value 747 | // 748 | // For performance reasons, we don't catch this error here and allow it to propagate up the call 749 | // stack. Note that you'll get the same error in JavaScript if you try to access a property using 750 | // such a 'broken' object as a key. 751 | return name + ""; 752 | } 753 | 754 | var OPERATORS = createMap(); 755 | forEach( 756 | "+ - * / % === !== == != < > <= >= && || ! = |".split(" "), 757 | function (operator) { 758 | OPERATORS[operator] = true; 759 | } 760 | ); 761 | var ESCAPE = { 762 | n: "\n", 763 | f: "\f", 764 | r: "\r", 765 | t: "\t", 766 | v: "\v", 767 | "'": "'", 768 | '"': '"', 769 | }; 770 | 771 | ///////////////////////////////////////// 772 | 773 | /** 774 | * @constructor 775 | */ 776 | function Lexer(options) { 777 | this.options = options || {}; 778 | } 779 | 780 | Lexer.prototype = { 781 | constructor: Lexer, 782 | 783 | lex: function (text) { 784 | this.text = text; 785 | this.index = 0; 786 | this.tokens = []; 787 | 788 | while (this.index < this.text.length) { 789 | var ch = this.text.charAt(this.index); 790 | if (ch === '"' || ch === "'" || ch === "`") { 791 | this.readString(ch); 792 | } else if ( 793 | this.isNumber(ch) || 794 | (ch === "." && this.isNumber(this.peek())) 795 | ) { 796 | this.readNumber(); 797 | } else if (this.isIdentifierStart(this.peekMultichar())) { 798 | this.readIdent(); 799 | } else if (this.is(ch, "(){}[].,;:?")) { 800 | this.tokens.push({ index: this.index, text: ch }); 801 | this.index++; 802 | } else if (this.isWhitespace(ch)) { 803 | this.index++; 804 | } else { 805 | var ch2 = ch + this.peek(); 806 | var ch3 = ch2 + this.peek(2); 807 | var op1 = OPERATORS[ch]; 808 | var op2 = OPERATORS[ch2]; 809 | var op3 = OPERATORS[ch3]; 810 | if (op1 || op2 || op3) { 811 | var token = op3 ? ch3 : op2 ? ch2 : ch; 812 | this.tokens.push({ index: this.index, text: token, operator: true }); 813 | this.index += token.length; 814 | } else { 815 | this.throwError( 816 | "Unexpected next character ", 817 | this.index, 818 | this.index + 1 819 | ); 820 | } 821 | } 822 | } 823 | return this.tokens; 824 | }, 825 | 826 | is: function (ch, chars) { 827 | return chars.indexOf(ch) !== -1; 828 | }, 829 | 830 | peek: function (i) { 831 | var num = i || 1; 832 | return this.index + num < this.text.length 833 | ? this.text.charAt(this.index + num) 834 | : false; 835 | }, 836 | 837 | isNumber: function (ch) { 838 | return ch >= "0" && ch <= "9" && typeof ch === "string"; 839 | }, 840 | 841 | isWhitespace: function (ch) { 842 | // IE treats non-breaking space as \u00A0 843 | return ( 844 | ch === " " || 845 | ch === "\r" || 846 | ch === "\t" || 847 | ch === "\n" || 848 | ch === "\v" || 849 | ch === "\u00A0" 850 | ); 851 | }, 852 | 853 | isIdentifierStart: function (ch) { 854 | return this.options.isIdentifierStart 855 | ? this.options.isIdentifierStart(ch, this.codePointAt(ch)) 856 | : this.isValidIdentifierStart(ch); 857 | }, 858 | 859 | isValidIdentifierStart: function (ch) { 860 | return ( 861 | (ch >= "a" && ch <= "z") || 862 | (ch >= "A" && ch <= "Z") || 863 | ch === "_" || 864 | ch === "$" 865 | ); 866 | }, 867 | 868 | isIdentifierContinue: function (ch) { 869 | return this.options.isIdentifierContinue 870 | ? this.options.isIdentifierContinue(ch, this.codePointAt(ch)) 871 | : this.isValidIdentifierContinue(ch); 872 | }, 873 | 874 | isValidIdentifierContinue: function (ch, cp) { 875 | return this.isValidIdentifierStart(ch, cp) || this.isNumber(ch); 876 | }, 877 | 878 | codePointAt: function (ch) { 879 | if (ch.length === 1) { 880 | return ch.charCodeAt(0); 881 | } 882 | 883 | return (ch.charCodeAt(0) << 10) + ch.charCodeAt(1) - 0x35fdc00; 884 | }, 885 | 886 | peekMultichar: function () { 887 | var ch = this.text.charAt(this.index); 888 | var peek = this.peek(); 889 | if (!peek) { 890 | return ch; 891 | } 892 | var cp1 = ch.charCodeAt(0); 893 | var cp2 = peek.charCodeAt(0); 894 | if (cp1 >= 0xd800 && cp1 <= 0xdbff && cp2 >= 0xdc00 && cp2 <= 0xdfff) { 895 | return ch + peek; 896 | } 897 | return ch; 898 | }, 899 | 900 | isExpOperator: function (ch) { 901 | return ch === "-" || ch === "+" || this.isNumber(ch); 902 | }, 903 | 904 | throwError: function (error, start, end) { 905 | end = end || this.index; 906 | var colStr = isDefined(start) 907 | ? "s " + 908 | start + 909 | "-" + 910 | this.index + 911 | " [" + 912 | this.text.substring(start, end) + 913 | "]" 914 | : " " + end; 915 | throw $parseMinErr( 916 | "lexerr", 917 | "Lexer Error: {0} at column{1} in expression [{2}].", 918 | error, 919 | colStr, 920 | this.text 921 | ); 922 | }, 923 | 924 | readNumber: function () { 925 | var number = ""; 926 | var start = this.index; 927 | while (this.index < this.text.length) { 928 | var ch = lowercase(this.text.charAt(this.index)); 929 | if (ch === "." || this.isNumber(ch)) { 930 | number += ch; 931 | } else { 932 | var peekCh = this.peek(); 933 | if (ch === "e" && this.isExpOperator(peekCh)) { 934 | number += ch; 935 | } else if ( 936 | this.isExpOperator(ch) && 937 | peekCh && 938 | this.isNumber(peekCh) && 939 | number.charAt(number.length - 1) === "e" 940 | ) { 941 | number += ch; 942 | } else if ( 943 | this.isExpOperator(ch) && 944 | (!peekCh || !this.isNumber(peekCh)) && 945 | number.charAt(number.length - 1) === "e" 946 | ) { 947 | this.throwError("Invalid exponent"); 948 | } else { 949 | break; 950 | } 951 | } 952 | this.index++; 953 | } 954 | this.tokens.push({ 955 | index: start, 956 | text: number, 957 | constant: true, 958 | value: Number(number), 959 | }); 960 | }, 961 | 962 | readIdent: function () { 963 | var start = this.index; 964 | this.index += this.peekMultichar().length; 965 | while (this.index < this.text.length) { 966 | var ch = this.peekMultichar(); 967 | if (!this.isIdentifierContinue(ch)) { 968 | break; 969 | } 970 | this.index += ch.length; 971 | } 972 | this.tokens.push({ 973 | index: start, 974 | text: this.text.slice(start, this.index), 975 | identifier: true, 976 | }); 977 | }, 978 | 979 | readString: function (quote) { 980 | // quote will be ', " or ` 981 | var start = this.index; 982 | this.index++; 983 | var string = ""; 984 | var rawString = quote; 985 | var isTemplateLiteral = quote === "`"; 986 | var escape = false; 987 | while (this.index < this.text.length) { 988 | var ch = this.text.charAt(this.index); 989 | if ( 990 | isTemplateLiteral && 991 | ch === "$" && 992 | this.text.charAt(this.index + 1) === "{" 993 | ) { 994 | this.tokens.push({ 995 | index: start, 996 | text: rawString, 997 | constant: true, 998 | value: string, 999 | }); 1000 | var inside = this.text.indexOf("}", this.index); 1001 | var myVariable = this.text.substr( 1002 | this.index + 2, 1003 | inside - this.index - 2 1004 | ); 1005 | this.tokens.push({ index: this.index, text: "+", operator: true }); 1006 | var lexed = new Lexer(this.options).lex(myVariable); 1007 | for (var i = 0, len = lexed.length; i < len; i++) { 1008 | this.tokens.push(lexed[i]); 1009 | } 1010 | this.tokens.push({ index: this.index, text: "+", operator: true }); 1011 | this.index = inside; 1012 | this.readString("`"); 1013 | return; 1014 | } 1015 | rawString += ch; 1016 | if (escape) { 1017 | if (ch === "u") { 1018 | var hex = this.text.substring(this.index + 1, this.index + 5); 1019 | if (!hex.match(/[\da-f]{4}/i)) { 1020 | this.throwError("Invalid unicode escape [\\u" + hex + "]"); 1021 | } 1022 | this.index += 4; 1023 | string += String.fromCharCode(parseInt(hex, 16)); 1024 | } else { 1025 | var rep = ESCAPE[ch]; 1026 | string = string + (rep || ch); 1027 | } 1028 | escape = false; 1029 | } else if (ch === "\\") { 1030 | escape = true; 1031 | } else if (ch === quote) { 1032 | // Matching closing quote 1033 | this.index++; 1034 | this.tokens.push({ 1035 | index: start, 1036 | text: rawString, 1037 | constant: true, 1038 | value: string, 1039 | }); 1040 | return; 1041 | } else { 1042 | string += ch; 1043 | } 1044 | this.index++; 1045 | } 1046 | this.throwError("Unterminated quote", start); 1047 | }, 1048 | }; 1049 | 1050 | function AST(lexer, options) { 1051 | this.lexer = lexer; 1052 | this.options = options; 1053 | } 1054 | 1055 | AST.Program = "Program"; 1056 | AST.ExpressionStatement = "ExpressionStatement"; 1057 | AST.AssignmentExpression = "AssignmentExpression"; 1058 | AST.ConditionalExpression = "ConditionalExpression"; 1059 | AST.LogicalExpression = "LogicalExpression"; 1060 | AST.BinaryExpression = "BinaryExpression"; 1061 | AST.UnaryExpression = "UnaryExpression"; 1062 | AST.CallExpression = "CallExpression"; 1063 | AST.MemberExpression = "MemberExpression"; 1064 | AST.Identifier = "Identifier"; 1065 | AST.Literal = "Literal"; 1066 | AST.ArrayExpression = "ArrayExpression"; 1067 | AST.Property = "Property"; 1068 | AST.ObjectExpression = "ObjectExpression"; 1069 | AST.ThisExpression = "ThisExpression"; 1070 | AST.LocalsExpression = "LocalsExpression"; 1071 | 1072 | // Internal use only 1073 | AST.NGValueParameter = "NGValueParameter"; 1074 | 1075 | AST.prototype = { 1076 | ast: function (text) { 1077 | this.text = text; 1078 | this.tokens = this.lexer.lex(text); 1079 | 1080 | var value = this.program(); 1081 | 1082 | if (this.tokens.length !== 0) { 1083 | this.throwError("is an unexpected token", this.tokens[0]); 1084 | } 1085 | 1086 | return value; 1087 | }, 1088 | 1089 | program: function () { 1090 | var body = []; 1091 | while (true) { 1092 | if (this.tokens.length > 0 && !this.peek("}", ")", ";", "]")) { 1093 | body.push(this.expressionStatement()); 1094 | } 1095 | if (!this.expect(";")) { 1096 | return { type: AST.Program, body: body }; 1097 | } 1098 | } 1099 | }, 1100 | 1101 | expressionStatement: function () { 1102 | return { type: AST.ExpressionStatement, expression: this.filterChain() }; 1103 | }, 1104 | 1105 | filterChain: function () { 1106 | var left = this.expression(); 1107 | while (this.expect("|")) { 1108 | left = this.filter(left); 1109 | } 1110 | return left; 1111 | }, 1112 | 1113 | expression: function () { 1114 | return this.assignment(); 1115 | }, 1116 | 1117 | assignment: function () { 1118 | var result = this.ternary(); 1119 | if (this.expect("=")) { 1120 | if (!isAssignable(result)) { 1121 | throw $parseMinErr("lval", "Trying to assign a value to a non l-value"); 1122 | } 1123 | 1124 | result = { 1125 | type: AST.AssignmentExpression, 1126 | left: result, 1127 | right: this.assignment(), 1128 | operator: "=", 1129 | }; 1130 | } 1131 | return result; 1132 | }, 1133 | 1134 | ternary: function () { 1135 | var test = this.logicalOR(); 1136 | var alternate; 1137 | var consequent; 1138 | if (this.expect("?")) { 1139 | alternate = this.expression(); 1140 | if (this.consume(":")) { 1141 | consequent = this.expression(); 1142 | return { 1143 | type: AST.ConditionalExpression, 1144 | test: test, 1145 | alternate: alternate, 1146 | consequent: consequent, 1147 | }; 1148 | } 1149 | } 1150 | return test; 1151 | }, 1152 | 1153 | logicalOR: function () { 1154 | var left = this.logicalAND(); 1155 | while (this.expect("||")) { 1156 | left = { 1157 | type: AST.LogicalExpression, 1158 | operator: "||", 1159 | left: left, 1160 | right: this.logicalAND(), 1161 | }; 1162 | } 1163 | return left; 1164 | }, 1165 | 1166 | logicalAND: function () { 1167 | var left = this.equality(); 1168 | while (this.expect("&&")) { 1169 | left = { 1170 | type: AST.LogicalExpression, 1171 | operator: "&&", 1172 | left: left, 1173 | right: this.equality(), 1174 | }; 1175 | } 1176 | return left; 1177 | }, 1178 | 1179 | equality: function () { 1180 | var left = this.relational(); 1181 | var token; 1182 | while ((token = this.expect("==", "!=", "===", "!=="))) { 1183 | left = { 1184 | type: AST.BinaryExpression, 1185 | operator: token.text, 1186 | left: left, 1187 | right: this.relational(), 1188 | }; 1189 | } 1190 | return left; 1191 | }, 1192 | 1193 | relational: function () { 1194 | var left = this.additive(); 1195 | var token; 1196 | while ((token = this.expect("<", ">", "<=", ">="))) { 1197 | left = { 1198 | type: AST.BinaryExpression, 1199 | operator: token.text, 1200 | left: left, 1201 | right: this.additive(), 1202 | }; 1203 | } 1204 | return left; 1205 | }, 1206 | 1207 | additive: function () { 1208 | var left = this.multiplicative(); 1209 | var token; 1210 | while ((token = this.expect("+", "-"))) { 1211 | left = { 1212 | type: AST.BinaryExpression, 1213 | operator: token.text, 1214 | left: left, 1215 | right: this.multiplicative(), 1216 | }; 1217 | } 1218 | return left; 1219 | }, 1220 | 1221 | multiplicative: function () { 1222 | var left = this.unary(); 1223 | var token; 1224 | while ((token = this.expect("*", "/", "%"))) { 1225 | left = { 1226 | type: AST.BinaryExpression, 1227 | operator: token.text, 1228 | left: left, 1229 | right: this.unary(), 1230 | }; 1231 | } 1232 | return left; 1233 | }, 1234 | 1235 | unary: function () { 1236 | var token; 1237 | if ((token = this.expect("+", "-", "!"))) { 1238 | return { 1239 | type: AST.UnaryExpression, 1240 | operator: token.text, 1241 | prefix: true, 1242 | argument: this.unary(), 1243 | }; 1244 | } 1245 | return this.primary(); 1246 | }, 1247 | 1248 | primary: function () { 1249 | var primary; 1250 | if (this.expect("(")) { 1251 | primary = this.filterChain(); 1252 | this.consume(")"); 1253 | } else if (this.expect("[")) { 1254 | primary = this.arrayDeclaration(); 1255 | } else if (this.expect("{")) { 1256 | primary = this.object(); 1257 | } else if (this.selfReferential.hasOwnProperty(this.peek().text)) { 1258 | primary = copy(this.selfReferential[this.consume().text]); 1259 | } else if (this.options.literals.hasOwnProperty(this.peek().text)) { 1260 | primary = { 1261 | type: AST.Literal, 1262 | value: this.options.literals[this.consume().text], 1263 | }; 1264 | } else if (this.peek().identifier) { 1265 | primary = this.identifier(); 1266 | } else if (this.peek().constant) { 1267 | primary = this.constant(); 1268 | } else { 1269 | this.throwError("not a primary expression", this.peek()); 1270 | } 1271 | 1272 | var next; 1273 | while ((next = this.expect("(", "[", "."))) { 1274 | if (next.text === "(") { 1275 | primary = { 1276 | type: AST.CallExpression, 1277 | callee: primary, 1278 | arguments: this.parseArguments(), 1279 | }; 1280 | this.consume(")"); 1281 | } else if (next.text === "[") { 1282 | primary = { 1283 | type: AST.MemberExpression, 1284 | object: primary, 1285 | property: this.expression(), 1286 | computed: true, 1287 | }; 1288 | this.consume("]"); 1289 | } else if (next.text === ".") { 1290 | primary = { 1291 | type: AST.MemberExpression, 1292 | object: primary, 1293 | property: this.identifier(), 1294 | computed: false, 1295 | }; 1296 | } else { 1297 | this.throwError("IMPOSSIBLE"); 1298 | } 1299 | } 1300 | return primary; 1301 | }, 1302 | 1303 | filter: function (baseExpression) { 1304 | var args = [baseExpression]; 1305 | var result = { 1306 | type: AST.CallExpression, 1307 | callee: this.identifier(), 1308 | arguments: args, 1309 | filter: true, 1310 | }; 1311 | 1312 | while (this.expect(":")) { 1313 | args.push(this.expression()); 1314 | } 1315 | 1316 | return result; 1317 | }, 1318 | 1319 | parseArguments: function () { 1320 | var args = []; 1321 | if (this.peekToken().text !== ")") { 1322 | do { 1323 | args.push(this.filterChain()); 1324 | } while (this.expect(",")); 1325 | } 1326 | return args; 1327 | }, 1328 | 1329 | identifier: function () { 1330 | var token = this.consume(); 1331 | if (!token.identifier) { 1332 | this.throwError("is not a valid identifier", token); 1333 | } 1334 | return { type: AST.Identifier, name: token.text }; 1335 | }, 1336 | 1337 | constant: function () { 1338 | // TODO check that it is a constant 1339 | return { type: AST.Literal, value: this.consume().value }; 1340 | }, 1341 | 1342 | arrayDeclaration: function () { 1343 | var elements = []; 1344 | if (this.peekToken().text !== "]") { 1345 | do { 1346 | if (this.peek("]")) { 1347 | // Support trailing commas per ES5.1. 1348 | break; 1349 | } 1350 | elements.push(this.expression()); 1351 | } while (this.expect(",")); 1352 | } 1353 | this.consume("]"); 1354 | 1355 | return { type: AST.ArrayExpression, elements: elements }; 1356 | }, 1357 | 1358 | object: function () { 1359 | var properties = [], 1360 | property; 1361 | if (this.peekToken().text !== "}") { 1362 | do { 1363 | if (this.peek("}")) { 1364 | // Support trailing commas per ES5.1. 1365 | break; 1366 | } 1367 | property = { type: AST.Property, kind: "init" }; 1368 | if (this.peek().constant) { 1369 | property.key = this.constant(); 1370 | property.computed = false; 1371 | this.consume(":"); 1372 | property.value = this.expression(); 1373 | } else if (this.peek().identifier) { 1374 | property.key = this.identifier(); 1375 | property.computed = false; 1376 | if (this.peek(":")) { 1377 | this.consume(":"); 1378 | property.value = this.expression(); 1379 | } else { 1380 | property.value = property.key; 1381 | } 1382 | } else if (this.peek("[")) { 1383 | this.consume("["); 1384 | property.key = this.expression(); 1385 | this.consume("]"); 1386 | property.computed = true; 1387 | this.consume(":"); 1388 | property.value = this.expression(); 1389 | } else { 1390 | this.throwError("invalid key", this.peek()); 1391 | } 1392 | properties.push(property); 1393 | } while (this.expect(",")); 1394 | } 1395 | this.consume("}"); 1396 | 1397 | return { type: AST.ObjectExpression, properties: properties }; 1398 | }, 1399 | 1400 | throwError: function (msg, token) { 1401 | throw $parseMinErr( 1402 | "syntax", 1403 | "Syntax Error: Token '{0}' {1} at column {2} of the expression [{3}] starting at [{4}].", 1404 | token.text, 1405 | msg, 1406 | token.index + 1, 1407 | this.text, 1408 | this.text.substring(token.index) 1409 | ); 1410 | }, 1411 | 1412 | consume: function (e1) { 1413 | if (this.tokens.length === 0) { 1414 | throw $parseMinErr( 1415 | "ueoe", 1416 | "Unexpected end of expression: {0}", 1417 | this.text 1418 | ); 1419 | } 1420 | 1421 | var token = this.expect(e1); 1422 | if (!token) { 1423 | this.throwError("is unexpected, expecting [" + e1 + "]", this.peek()); 1424 | } 1425 | return token; 1426 | }, 1427 | 1428 | peekToken: function () { 1429 | if (this.tokens.length === 0) { 1430 | throw $parseMinErr( 1431 | "ueoe", 1432 | "Unexpected end of expression: {0}", 1433 | this.text 1434 | ); 1435 | } 1436 | return this.tokens[0]; 1437 | }, 1438 | 1439 | peek: function (e1, e2, e3, e4) { 1440 | return this.peekAhead(0, e1, e2, e3, e4); 1441 | }, 1442 | 1443 | peekAhead: function (i, e1, e2, e3, e4) { 1444 | if (this.tokens.length > i) { 1445 | var token = this.tokens[i]; 1446 | var t = token.text; 1447 | if ( 1448 | t === e1 || 1449 | t === e2 || 1450 | t === e3 || 1451 | t === e4 || 1452 | (!e1 && !e2 && !e3 && !e4) 1453 | ) { 1454 | return token; 1455 | } 1456 | } 1457 | return false; 1458 | }, 1459 | 1460 | expect: function (e1, e2, e3, e4) { 1461 | var token = this.peek(e1, e2, e3, e4); 1462 | if (token) { 1463 | this.tokens.shift(); 1464 | return token; 1465 | } 1466 | return false; 1467 | }, 1468 | }; 1469 | 1470 | function ifDefined(v, d) { 1471 | return typeof v !== "undefined" ? v : d; 1472 | } 1473 | 1474 | function plusFn(l, r) { 1475 | if (typeof l === "undefined") { 1476 | return r; 1477 | } 1478 | if (typeof r === "undefined") { 1479 | return l; 1480 | } 1481 | return l + r; 1482 | } 1483 | 1484 | function isStateless($filter, filterName) { 1485 | var fn = $filter(filterName); 1486 | if (!fn) { 1487 | throw new Error("Filter '" + filterName + "' is not defined"); 1488 | } 1489 | return !fn.$stateful; 1490 | } 1491 | 1492 | function findConstantAndWatchExpressions(ast, $filter) { 1493 | var allConstants; 1494 | var argsToWatch; 1495 | var isStatelessFilter; 1496 | switch (ast.type) { 1497 | case AST.Program: 1498 | allConstants = true; 1499 | forEach(ast.body, function (expr) { 1500 | findConstantAndWatchExpressions(expr.expression, $filter); 1501 | allConstants = allConstants && expr.expression.constant; 1502 | }); 1503 | ast.constant = allConstants; 1504 | break; 1505 | case AST.Literal: 1506 | ast.constant = true; 1507 | ast.toWatch = []; 1508 | break; 1509 | case AST.UnaryExpression: 1510 | findConstantAndWatchExpressions(ast.argument, $filter); 1511 | ast.constant = ast.argument.constant; 1512 | ast.toWatch = ast.argument.toWatch; 1513 | break; 1514 | case AST.BinaryExpression: 1515 | findConstantAndWatchExpressions(ast.left, $filter); 1516 | findConstantAndWatchExpressions(ast.right, $filter); 1517 | ast.constant = ast.left.constant && ast.right.constant; 1518 | ast.toWatch = ast.left.toWatch.concat(ast.right.toWatch); 1519 | break; 1520 | case AST.LogicalExpression: 1521 | findConstantAndWatchExpressions(ast.left, $filter); 1522 | findConstantAndWatchExpressions(ast.right, $filter); 1523 | ast.constant = ast.left.constant && ast.right.constant; 1524 | ast.toWatch = ast.constant ? [] : [ast]; 1525 | break; 1526 | case AST.ConditionalExpression: 1527 | findConstantAndWatchExpressions(ast.test, $filter); 1528 | findConstantAndWatchExpressions(ast.alternate, $filter); 1529 | findConstantAndWatchExpressions(ast.consequent, $filter); 1530 | ast.constant = 1531 | ast.test.constant && ast.alternate.constant && ast.consequent.constant; 1532 | ast.toWatch = ast.constant ? [] : [ast]; 1533 | break; 1534 | case AST.Identifier: 1535 | ast.constant = false; 1536 | ast.toWatch = [ast]; 1537 | break; 1538 | case AST.MemberExpression: 1539 | findConstantAndWatchExpressions(ast.object, $filter); 1540 | if (ast.computed) { 1541 | findConstantAndWatchExpressions(ast.property, $filter); 1542 | } 1543 | ast.constant = 1544 | ast.object.constant && (!ast.computed || ast.property.constant); 1545 | ast.toWatch = [ast]; 1546 | break; 1547 | case AST.CallExpression: 1548 | isStatelessFilter = ast.filter 1549 | ? isStateless($filter, ast.callee.name) 1550 | : false; 1551 | allConstants = isStatelessFilter; 1552 | argsToWatch = []; 1553 | forEach(ast.arguments, function (expr) { 1554 | findConstantAndWatchExpressions(expr, $filter); 1555 | allConstants = allConstants && expr.constant; 1556 | if (!expr.constant) { 1557 | argsToWatch.push.apply(argsToWatch, expr.toWatch); 1558 | } 1559 | }); 1560 | ast.constant = allConstants; 1561 | ast.toWatch = isStatelessFilter ? argsToWatch : [ast]; 1562 | break; 1563 | case AST.AssignmentExpression: 1564 | findConstantAndWatchExpressions(ast.left, $filter); 1565 | findConstantAndWatchExpressions(ast.right, $filter); 1566 | ast.constant = ast.left.constant && ast.right.constant; 1567 | ast.toWatch = [ast]; 1568 | break; 1569 | case AST.ArrayExpression: 1570 | allConstants = true; 1571 | argsToWatch = []; 1572 | forEach(ast.elements, function (expr) { 1573 | findConstantAndWatchExpressions(expr, $filter); 1574 | allConstants = allConstants && expr.constant; 1575 | if (!expr.constant) { 1576 | argsToWatch.push.apply(argsToWatch, expr.toWatch); 1577 | } 1578 | }); 1579 | ast.constant = allConstants; 1580 | ast.toWatch = argsToWatch; 1581 | break; 1582 | case AST.ObjectExpression: 1583 | allConstants = true; 1584 | argsToWatch = []; 1585 | forEach(ast.properties, function (property) { 1586 | findConstantAndWatchExpressions(property.value, $filter); 1587 | allConstants = 1588 | allConstants && property.value.constant && !property.computed; 1589 | if (!property.value.constant) { 1590 | argsToWatch.push.apply(argsToWatch, property.value.toWatch); 1591 | } 1592 | }); 1593 | ast.constant = allConstants; 1594 | ast.toWatch = argsToWatch; 1595 | break; 1596 | case AST.ThisExpression: 1597 | ast.constant = false; 1598 | ast.toWatch = []; 1599 | break; 1600 | case AST.LocalsExpression: 1601 | ast.constant = false; 1602 | ast.toWatch = []; 1603 | break; 1604 | } 1605 | } 1606 | 1607 | function getInputs(body) { 1608 | if (body.length !== 1) { 1609 | return; 1610 | } 1611 | var lastExpression = body[0].expression; 1612 | var candidate = lastExpression.toWatch; 1613 | if (candidate.length !== 1) { 1614 | return candidate; 1615 | } 1616 | return candidate[0] !== lastExpression ? candidate : undefined; 1617 | } 1618 | 1619 | function isAssignable(ast) { 1620 | return ast.type === AST.Identifier || ast.type === AST.MemberExpression; 1621 | } 1622 | 1623 | function assignableAST(ast) { 1624 | if (ast.body.length === 1 && isAssignable(ast.body[0].expression)) { 1625 | return { 1626 | type: AST.AssignmentExpression, 1627 | left: ast.body[0].expression, 1628 | right: { type: AST.NGValueParameter }, 1629 | operator: "=", 1630 | }; 1631 | } 1632 | } 1633 | 1634 | function isLiteral(ast) { 1635 | return ( 1636 | ast.body.length === 0 || 1637 | (ast.body.length === 1 && 1638 | (ast.body[0].expression.type === AST.Literal || 1639 | ast.body[0].expression.type === AST.ArrayExpression || 1640 | ast.body[0].expression.type === AST.ObjectExpression)) 1641 | ); 1642 | } 1643 | 1644 | function isConstant(ast) { 1645 | return ast.constant; 1646 | } 1647 | 1648 | function ASTCompiler(astBuilder, $filter) { 1649 | this.astBuilder = astBuilder; 1650 | this.$filter = $filter; 1651 | } 1652 | 1653 | ASTCompiler.prototype = { 1654 | compile: function (expression) { 1655 | var self = this; 1656 | var ast = this.astBuilder.ast(expression); 1657 | this.state = { 1658 | nextId: 0, 1659 | filters: {}, 1660 | fn: { vars: [], body: [], own: {} }, 1661 | assign: { vars: [], body: [], own: {} }, 1662 | inputs: [], 1663 | }; 1664 | findConstantAndWatchExpressions(ast, self.$filter); 1665 | var extra = ""; 1666 | var assignable; 1667 | this.stage = "assign"; 1668 | if ((assignable = assignableAST(ast))) { 1669 | this.state.computing = "assign"; 1670 | var result = this.nextId(); 1671 | this.recurse(assignable, result); 1672 | this.return_(result); 1673 | extra = "fn.assign=" + this.generateFunction("assign", "s,v,l"); 1674 | } 1675 | var toWatch = getInputs(ast.body); 1676 | self.stage = "inputs"; 1677 | forEach(toWatch, function (watch, key) { 1678 | var fnKey = "fn" + key; 1679 | self.state[fnKey] = { vars: [], body: [], own: {} }; 1680 | self.state.computing = fnKey; 1681 | var intoId = self.nextId(); 1682 | self.recurse(watch, intoId); 1683 | self.return_(intoId); 1684 | self.state.inputs.push(fnKey); 1685 | watch.watchId = key; 1686 | }); 1687 | this.state.computing = "fn"; 1688 | this.stage = "main"; 1689 | this.recurse(ast); 1690 | var fnString = 1691 | // The build and minification steps remove the string "use strict" from the code, but this is done using a regex. 1692 | // This is a workaround for this until we do a better job at only removing the prefix only when we should. 1693 | '"' + 1694 | this.USE + 1695 | " " + 1696 | this.STRICT + 1697 | '";\n' + 1698 | this.filterPrefix() + 1699 | "var fn=" + 1700 | this.generateFunction("fn", "s,l,a,i") + 1701 | extra + 1702 | this.watchFns() + 1703 | "return fn;"; 1704 | 1705 | // eslint-disable-next-line no-new-func 1706 | var wrappedFn = new Function( 1707 | "$filter", 1708 | "getStringValue", 1709 | "ifDefined", 1710 | "plus", 1711 | fnString 1712 | )(this.$filter, getStringValue, ifDefined, plusFn); 1713 | 1714 | var fn = function (s, l, a, i) { 1715 | return runWithFunctionConstructorProtection(function () { 1716 | return wrappedFn(s, l, a, i); 1717 | }); 1718 | }; 1719 | fn.assign = function (s, v, l) { 1720 | return runWithFunctionConstructorProtection(function () { 1721 | return wrappedFn.assign(s, v, l); 1722 | }); 1723 | }; 1724 | fn.inputs = wrappedFn.inputs; 1725 | 1726 | this.state = this.stage = undefined; 1727 | fn.ast = ast; 1728 | fn.literal = isLiteral(ast); 1729 | fn.constant = isConstant(ast); 1730 | return fn; 1731 | }, 1732 | 1733 | USE: "use", 1734 | 1735 | STRICT: "strict", 1736 | 1737 | watchFns: function () { 1738 | var result = []; 1739 | var fns = this.state.inputs; 1740 | var self = this; 1741 | forEach(fns, function (name) { 1742 | result.push("var " + name + "=" + self.generateFunction(name, "s")); 1743 | }); 1744 | if (fns.length) { 1745 | result.push("fn.inputs=[" + fns.join(",") + "];"); 1746 | } 1747 | return result.join(""); 1748 | }, 1749 | 1750 | generateFunction: function (name, params) { 1751 | return ( 1752 | "function(" + 1753 | params + 1754 | "){" + 1755 | this.varsPrefix(name) + 1756 | this.body(name) + 1757 | "};" 1758 | ); 1759 | }, 1760 | 1761 | filterPrefix: function () { 1762 | var parts = []; 1763 | var self = this; 1764 | forEach(this.state.filters, function (id, filter) { 1765 | parts.push(id + "=$filter(" + self.escape(filter) + ")"); 1766 | }); 1767 | if (parts.length) { 1768 | return "var " + parts.join(",") + ";"; 1769 | } 1770 | return ""; 1771 | }, 1772 | 1773 | varsPrefix: function (section) { 1774 | return this.state[section].vars.length 1775 | ? "var " + this.state[section].vars.join(",") + ";" 1776 | : ""; 1777 | }, 1778 | 1779 | body: function (section) { 1780 | return this.state[section].body.join(""); 1781 | }, 1782 | 1783 | recurse: function ( 1784 | ast, 1785 | intoId, 1786 | nameId, 1787 | recursionFn, 1788 | create, 1789 | skipWatchIdCheck 1790 | ) { 1791 | var left, 1792 | right, 1793 | self = this, 1794 | args, 1795 | expression, 1796 | computed; 1797 | recursionFn = recursionFn || noop; 1798 | if (!skipWatchIdCheck && isDefined(ast.watchId)) { 1799 | intoId = intoId || this.nextId(); 1800 | this.if_( 1801 | "i", 1802 | this.lazyAssign(intoId, this.unsafeComputedMember("i", ast.watchId)), 1803 | this.lazyRecurse(ast, intoId, nameId, recursionFn, create, true) 1804 | ); 1805 | return; 1806 | } 1807 | 1808 | switch (ast.type) { 1809 | case AST.Program: 1810 | forEach(ast.body, function (expression, pos) { 1811 | self.recurse( 1812 | expression.expression, 1813 | undefined, 1814 | undefined, 1815 | function (expr) { 1816 | right = expr; 1817 | } 1818 | ); 1819 | if (pos !== ast.body.length - 1) { 1820 | self.current().body.push(right, ";"); 1821 | } else { 1822 | self.return_(right); 1823 | } 1824 | }); 1825 | break; 1826 | case AST.Literal: 1827 | expression = this.escape(ast.value); 1828 | this.assign(intoId, expression); 1829 | recursionFn(intoId || expression); 1830 | break; 1831 | case AST.UnaryExpression: 1832 | this.recurse(ast.argument, undefined, undefined, function (expr) { 1833 | right = expr; 1834 | }); 1835 | expression = ast.operator + "(" + this.ifDefined(right, 0) + ")"; 1836 | this.assign(intoId, expression); 1837 | recursionFn(expression); 1838 | break; 1839 | case AST.BinaryExpression: 1840 | this.recurse(ast.left, undefined, undefined, function (expr) { 1841 | left = expr; 1842 | }); 1843 | this.recurse(ast.right, undefined, undefined, function (expr) { 1844 | right = expr; 1845 | }); 1846 | if (ast.operator === "+") { 1847 | expression = this.plus(left, right); 1848 | } else if (ast.operator === "-") { 1849 | expression = 1850 | this.ifDefined(left, 0) + ast.operator + this.ifDefined(right, 0); 1851 | } else { 1852 | expression = "(" + left + ")" + ast.operator + "(" + right + ")"; 1853 | } 1854 | this.assign(intoId, expression); 1855 | recursionFn(expression); 1856 | break; 1857 | case AST.LogicalExpression: 1858 | intoId = intoId || this.nextId(); 1859 | self.recurse(ast.left, intoId); 1860 | self.if_( 1861 | ast.operator === "&&" ? intoId : self.not(intoId), 1862 | self.lazyRecurse(ast.right, intoId) 1863 | ); 1864 | recursionFn(intoId); 1865 | break; 1866 | case AST.ConditionalExpression: 1867 | intoId = intoId || this.nextId(); 1868 | self.recurse(ast.test, intoId); 1869 | self.if_( 1870 | intoId, 1871 | self.lazyRecurse(ast.alternate, intoId), 1872 | self.lazyRecurse(ast.consequent, intoId) 1873 | ); 1874 | recursionFn(intoId); 1875 | break; 1876 | case AST.Identifier: 1877 | intoId = intoId || this.nextId(); 1878 | var inAssignment = self.current().inAssignment; 1879 | if (nameId) { 1880 | if (inAssignment) { 1881 | nameId.context = this.assign(this.nextId(), "s"); 1882 | } else { 1883 | nameId.context = 1884 | self.stage === "inputs" 1885 | ? "s" 1886 | : this.assign( 1887 | this.nextId(), 1888 | this.getHasOwnProperty("l", ast.name) + "?l:s" 1889 | ); 1890 | } 1891 | nameId.computed = false; 1892 | nameId.name = ast.name; 1893 | } 1894 | self.if_( 1895 | self.stage === "inputs" || 1896 | self.not(self.getHasOwnProperty("l", ast.name)), 1897 | function () { 1898 | self.if_( 1899 | self.stage === "inputs" || 1900 | self.and_( 1901 | "s", 1902 | self.or_( 1903 | self.isNull(self.nonComputedMember("s", ast.name)), 1904 | self.hasOwnProperty_("s", ast.name) 1905 | ) 1906 | ), 1907 | function () { 1908 | if (create && create !== 1) { 1909 | self.if_( 1910 | self.isNull(self.nonComputedMember("s", ast.name)), 1911 | self.lazyAssign(self.nonComputedMember("s", ast.name), "{}") 1912 | ); 1913 | } 1914 | self.assign(intoId, self.nonComputedMember("s", ast.name)); 1915 | } 1916 | ); 1917 | }, 1918 | intoId && 1919 | function () { 1920 | self.if_( 1921 | self.hasOwnProperty_("l", ast.name), 1922 | self.lazyAssign(intoId, self.nonComputedMember("l", ast.name)) 1923 | ); 1924 | } 1925 | ); 1926 | recursionFn(intoId); 1927 | break; 1928 | case AST.MemberExpression: 1929 | left = (nameId && (nameId.context = this.nextId())) || this.nextId(); 1930 | intoId = intoId || this.nextId(); 1931 | self.recurse( 1932 | ast.object, 1933 | left, 1934 | undefined, 1935 | function () { 1936 | var member = null; 1937 | var inAssignment = self.current().inAssignment; 1938 | if (ast.computed) { 1939 | right = self.nextId(); 1940 | if (inAssignment || self.state.computing === "assign") { 1941 | member = self.unsafeComputedMember(left, right); 1942 | } else { 1943 | member = self.computedMember(left, right); 1944 | } 1945 | } else { 1946 | if (inAssignment || self.state.computing === "assign") { 1947 | member = self.unsafeNonComputedMember(left, ast.property.name); 1948 | } else { 1949 | member = self.nonComputedMember(left, ast.property.name); 1950 | } 1951 | right = ast.property.name; 1952 | } 1953 | 1954 | if (ast.computed) { 1955 | if (ast.property.type === AST.Literal) { 1956 | self.recurse(ast.property, right); 1957 | } 1958 | } 1959 | self.if_( 1960 | self.and_( 1961 | self.notNull(left), 1962 | self.or_( 1963 | self.isNull(member), 1964 | self.hasOwnProperty_(left, right, ast.computed) 1965 | ) 1966 | ), 1967 | function () { 1968 | if (ast.computed) { 1969 | if (ast.property.type !== AST.Literal) { 1970 | self.recurse(ast.property, right); 1971 | } 1972 | if (create && create !== 1) { 1973 | self.if_(self.not(member), self.lazyAssign(member, "{}")); 1974 | } 1975 | self.assign(intoId, member); 1976 | if (nameId) { 1977 | nameId.computed = true; 1978 | nameId.name = right; 1979 | } 1980 | } else { 1981 | if (create && create !== 1) { 1982 | self.if_( 1983 | self.isNull(member), 1984 | self.lazyAssign(member, "{}") 1985 | ); 1986 | } 1987 | self.assign(intoId, member); 1988 | if (nameId) { 1989 | nameId.computed = false; 1990 | nameId.name = ast.property.name; 1991 | } 1992 | } 1993 | }, 1994 | function () { 1995 | self.assign(intoId, "undefined"); 1996 | } 1997 | ); 1998 | recursionFn(intoId); 1999 | }, 2000 | !!create 2001 | ); 2002 | break; 2003 | case AST.CallExpression: 2004 | intoId = intoId || this.nextId(); 2005 | if (ast.filter) { 2006 | right = self.filter(ast.callee.name); 2007 | args = []; 2008 | forEach(ast.arguments, function (expr) { 2009 | var argument = self.nextId(); 2010 | self.recurse(expr, argument); 2011 | args.push(argument); 2012 | }); 2013 | expression = right + ".call(" + right + "," + args.join(",") + ")"; 2014 | self.assign(intoId, expression); 2015 | recursionFn(intoId); 2016 | } else { 2017 | right = self.nextId(); 2018 | left = {}; 2019 | args = []; 2020 | self.recurse(ast.callee, right, left, function () { 2021 | self.if_( 2022 | self.notNull(right), 2023 | function () { 2024 | forEach(ast.arguments, function (expr) { 2025 | self.recurse( 2026 | expr, 2027 | ast.constant ? undefined : self.nextId(), 2028 | undefined, 2029 | function (argument) { 2030 | args.push(argument); 2031 | } 2032 | ); 2033 | }); 2034 | if (left.name) { 2035 | var x = self.member(left.context, left.name, left.computed); 2036 | expression = 2037 | "(" + 2038 | x + 2039 | " === null ? null : " + 2040 | self.unsafeMember(left.context, left.name, left.computed) + 2041 | ".call(" + 2042 | [left.context].concat(args).join(",") + 2043 | "))"; 2044 | } else { 2045 | expression = right + "(" + args.join(",") + ")"; 2046 | } 2047 | self.assign(intoId, expression); 2048 | }, 2049 | function () { 2050 | self.assign(intoId, "undefined"); 2051 | } 2052 | ); 2053 | recursionFn(intoId); 2054 | }); 2055 | } 2056 | break; 2057 | case AST.AssignmentExpression: 2058 | right = this.nextId(); 2059 | left = {}; 2060 | self.current().inAssignment = true; 2061 | this.recurse( 2062 | ast.left, 2063 | undefined, 2064 | left, 2065 | function () { 2066 | self.if_( 2067 | self.and_( 2068 | self.notNull(left.context), 2069 | self.or_( 2070 | self.hasOwnProperty_(left.context, left.name), 2071 | self.isNull( 2072 | self.member(left.context, left.name, left.computed) 2073 | ) 2074 | ) 2075 | ), 2076 | function () { 2077 | self.recurse(ast.right, right); 2078 | expression = 2079 | self.member(left.context, left.name, left.computed) + 2080 | ast.operator + 2081 | right; 2082 | self.assign(intoId, expression); 2083 | recursionFn(intoId || expression); 2084 | } 2085 | ); 2086 | self.current().inAssignment = false; 2087 | self.recurse(ast.right, right); 2088 | self.current().inAssignment = true; 2089 | }, 2090 | 1 2091 | ); 2092 | self.current().inAssignment = false; 2093 | break; 2094 | case AST.ArrayExpression: 2095 | args = []; 2096 | forEach(ast.elements, function (expr) { 2097 | self.recurse( 2098 | expr, 2099 | ast.constant ? undefined : self.nextId(), 2100 | undefined, 2101 | function (argument) { 2102 | args.push(argument); 2103 | } 2104 | ); 2105 | }); 2106 | expression = "[" + args.join(",") + "]"; 2107 | this.assign(intoId, expression); 2108 | recursionFn(intoId || expression); 2109 | break; 2110 | case AST.ObjectExpression: 2111 | args = []; 2112 | computed = false; 2113 | forEach(ast.properties, function (property) { 2114 | if (property.computed) { 2115 | computed = true; 2116 | } 2117 | }); 2118 | if (computed) { 2119 | intoId = intoId || this.nextId(); 2120 | this.assign(intoId, "{}"); 2121 | forEach(ast.properties, function (property) { 2122 | if (property.computed) { 2123 | left = self.nextId(); 2124 | self.recurse(property.key, left); 2125 | } else { 2126 | left = 2127 | property.key.type === AST.Identifier 2128 | ? property.key.name 2129 | : "" + property.key.value; 2130 | } 2131 | right = self.nextId(); 2132 | self.recurse(property.value, right); 2133 | self.assign( 2134 | self.unsafeMember(intoId, left, property.computed), 2135 | right 2136 | ); 2137 | }); 2138 | } else { 2139 | forEach(ast.properties, function (property) { 2140 | self.recurse( 2141 | property.value, 2142 | ast.constant ? undefined : self.nextId(), 2143 | undefined, 2144 | function (expr) { 2145 | args.push( 2146 | self.escape( 2147 | property.key.type === AST.Identifier 2148 | ? property.key.name 2149 | : "" + property.key.value 2150 | ) + 2151 | ":" + 2152 | expr 2153 | ); 2154 | } 2155 | ); 2156 | }); 2157 | expression = "{" + args.join(",") + "}"; 2158 | this.assign(intoId, expression); 2159 | } 2160 | recursionFn(intoId || expression); 2161 | break; 2162 | case AST.ThisExpression: 2163 | this.assign(intoId, "s"); 2164 | recursionFn(intoId || "s"); 2165 | break; 2166 | case AST.LocalsExpression: 2167 | this.assign(intoId, "l"); 2168 | recursionFn(intoId || "l"); 2169 | break; 2170 | case AST.NGValueParameter: 2171 | this.assign(intoId, "v"); 2172 | recursionFn(intoId || "v"); 2173 | break; 2174 | } 2175 | }, 2176 | 2177 | getHasOwnProperty: function (element, property) { 2178 | var key = element + "." + property; 2179 | var own = this.current().own; 2180 | if (!own.hasOwnProperty(key)) { 2181 | own[key] = this.nextId( 2182 | false, 2183 | element + "&&(" + this.escape(property) + " in " + element + ")" 2184 | ); 2185 | } 2186 | return own[key]; 2187 | }, 2188 | 2189 | assign: function (id, value) { 2190 | if (!id) { 2191 | return; 2192 | } 2193 | this.current().body.push(id, "=", value, ";"); 2194 | return id; 2195 | }, 2196 | 2197 | filter: function (filterName) { 2198 | if (!hasOwnProperty.call(this.state.filters, filterName)) { 2199 | this.state.filters[filterName] = this.nextId(true); 2200 | } 2201 | return this.state.filters[filterName]; 2202 | }, 2203 | 2204 | ifDefined: function (id, defaultValue) { 2205 | return "ifDefined(" + id + "," + this.escape(defaultValue) + ")"; 2206 | }, 2207 | 2208 | plus: function (left, right) { 2209 | return "plus(" + left + "," + right + ")"; 2210 | }, 2211 | 2212 | return_: function (id) { 2213 | this.current().body.push("return ", id, ";"); 2214 | }, 2215 | 2216 | if_: function (test, alternate, consequent) { 2217 | if (test === true) { 2218 | alternate(); 2219 | } else { 2220 | var body = this.current().body; 2221 | body.push("if(", test, "){"); 2222 | alternate(); 2223 | body.push("}"); 2224 | if (consequent) { 2225 | body.push("else{"); 2226 | consequent(); 2227 | body.push("}"); 2228 | } 2229 | } 2230 | }, 2231 | or_: function (expr1, expr2) { 2232 | return "(" + expr1 + ") || (" + expr2 + ")"; 2233 | }, 2234 | hasOwnProperty_: function (obj, prop, computed) { 2235 | if (computed) { 2236 | return "(Object.prototype.hasOwnProperty.call(" + obj + "," + prop + "))"; 2237 | } 2238 | return "(Object.prototype.hasOwnProperty.call(" + obj + ",'" + prop + "'))"; 2239 | }, 2240 | and_: function (expr1, expr2) { 2241 | return "(" + expr1 + ") && (" + expr2 + ")"; 2242 | }, 2243 | not: function (expression) { 2244 | return "!(" + expression + ")"; 2245 | }, 2246 | 2247 | isNull: function (expression) { 2248 | return expression + "==null"; 2249 | }, 2250 | 2251 | notNull: function (expression) { 2252 | return expression + "!=null"; 2253 | }, 2254 | 2255 | nonComputedMember: function (left, right) { 2256 | var SAFE_IDENTIFIER = /^[$_a-zA-Z][$_a-zA-Z0-9]*$/; 2257 | var UNSAFE_CHARACTERS = /[^$_a-zA-Z0-9]/g; 2258 | var expr = ""; 2259 | if (SAFE_IDENTIFIER.test(right)) { 2260 | expr = left + "." + right; 2261 | } else { 2262 | right = right.replace(UNSAFE_CHARACTERS, this.stringEscapeFn); 2263 | expr = left + '["' + right + '"]'; 2264 | } 2265 | 2266 | return expr; 2267 | }, 2268 | 2269 | unsafeComputedMember: function (left, right) { 2270 | return left + "[" + right + "]"; 2271 | }, 2272 | unsafeNonComputedMember: function (left, right) { 2273 | return this.nonComputedMember(left, right); 2274 | }, 2275 | 2276 | computedMember: function (left, right) { 2277 | if (this.state.computing === "assign") { 2278 | return this.unsafeComputedMember(left, right); 2279 | } 2280 | // return left + "[" + right + "]"; 2281 | return ( 2282 | "(" + 2283 | left + 2284 | ".hasOwnProperty(" + 2285 | right + 2286 | ") ? " + 2287 | left + 2288 | "[" + 2289 | right + 2290 | "] : undefined)" 2291 | ); 2292 | }, 2293 | 2294 | unsafeMember: function (left, right, computed) { 2295 | if (computed) { 2296 | return this.unsafeComputedMember(left, right); 2297 | } 2298 | return this.unsafeNonComputedMember(left, right); 2299 | }, 2300 | 2301 | member: function (left, right, computed) { 2302 | if (computed) { 2303 | return this.computedMember(left, right); 2304 | } 2305 | return this.nonComputedMember(left, right); 2306 | }, 2307 | 2308 | getStringValue: function (item) { 2309 | this.assign(item, "getStringValue(" + item + ")"); 2310 | }, 2311 | 2312 | lazyRecurse: function ( 2313 | ast, 2314 | intoId, 2315 | nameId, 2316 | recursionFn, 2317 | create, 2318 | skipWatchIdCheck 2319 | ) { 2320 | var self = this; 2321 | return function () { 2322 | self.recurse(ast, intoId, nameId, recursionFn, create, skipWatchIdCheck); 2323 | }; 2324 | }, 2325 | 2326 | lazyAssign: function (id, value) { 2327 | var self = this; 2328 | return function () { 2329 | self.assign(id, value); 2330 | }; 2331 | }, 2332 | 2333 | stringEscapeRegex: /[^ a-zA-Z0-9]/g, 2334 | 2335 | stringEscapeFn: function (c) { 2336 | return "\\u" + ("0000" + c.charCodeAt(0).toString(16)).slice(-4); 2337 | }, 2338 | 2339 | escape: function (value) { 2340 | if (isString(value)) { 2341 | return ( 2342 | "'" + value.replace(this.stringEscapeRegex, this.stringEscapeFn) + "'" 2343 | ); 2344 | } 2345 | if (isNumber(value)) { 2346 | return value.toString(); 2347 | } 2348 | if (value === true) { 2349 | return "true"; 2350 | } 2351 | if (value === false) { 2352 | return "false"; 2353 | } 2354 | if (value === null) { 2355 | return "null"; 2356 | } 2357 | if (typeof value === "undefined") { 2358 | return "undefined"; 2359 | } 2360 | 2361 | throw $parseMinErr("esc", "IMPOSSIBLE"); 2362 | }, 2363 | 2364 | nextId: function (skip, init) { 2365 | var id = "v" + this.state.nextId++; 2366 | if (!skip) { 2367 | this.current().vars.push(id + (init ? "=" + init : "")); 2368 | } 2369 | return id; 2370 | }, 2371 | 2372 | current: function () { 2373 | return this.state[this.state.computing]; 2374 | }, 2375 | }; 2376 | 2377 | function ASTInterpreter(astBuilder, $filter) { 2378 | this.astBuilder = astBuilder; 2379 | this.$filter = $filter; 2380 | } 2381 | 2382 | ASTInterpreter.prototype = { 2383 | compile: function (expression) { 2384 | var self = this; 2385 | var ast = this.astBuilder.ast(expression); 2386 | findConstantAndWatchExpressions(ast, self.$filter); 2387 | var assignable; 2388 | var assign; 2389 | if ((assignable = assignableAST(ast))) { 2390 | assign = this.recurse(assignable); 2391 | } 2392 | var toWatch = getInputs(ast.body); 2393 | var inputs; 2394 | if (toWatch) { 2395 | inputs = []; 2396 | forEach(toWatch, function (watch, key) { 2397 | var input = self.recurse(watch); 2398 | watch.input = input; 2399 | inputs.push(input); 2400 | watch.watchId = key; 2401 | }); 2402 | } 2403 | var expressions = []; 2404 | forEach(ast.body, function (expression) { 2405 | expressions.push(self.recurse(expression.expression)); 2406 | }); 2407 | var wrappedFn = 2408 | ast.body.length === 0 2409 | ? noop 2410 | : ast.body.length === 1 2411 | ? expressions[0] 2412 | : function (scope, locals) { 2413 | var lastValue; 2414 | forEach(expressions, function (exp) { 2415 | lastValue = exp(scope, locals); 2416 | }); 2417 | return lastValue; 2418 | }; 2419 | 2420 | if (assign) { 2421 | wrappedFn.assign = function (scope, value, locals) { 2422 | return assign(scope, locals, value); 2423 | }; 2424 | } 2425 | 2426 | var fn = function (scope, locals) { 2427 | return runWithFunctionConstructorProtection(function () { 2428 | return wrappedFn(scope, locals); 2429 | }); 2430 | }; 2431 | fn.assign = function (scope, value, locals) { 2432 | return runWithFunctionConstructorProtection(function () { 2433 | return wrappedFn.assign(scope, value, locals); 2434 | }); 2435 | }; 2436 | 2437 | if (inputs) { 2438 | fn.inputs = inputs; 2439 | } 2440 | fn.ast = ast; 2441 | fn.literal = isLiteral(ast); 2442 | fn.constant = isConstant(ast); 2443 | return fn; 2444 | }, 2445 | 2446 | recurse: function (ast, context, create) { 2447 | var left, 2448 | right, 2449 | self = this, 2450 | args; 2451 | if (ast.input) { 2452 | return this.inputs(ast.input, ast.watchId); 2453 | } 2454 | switch (ast.type) { 2455 | case AST.Literal: 2456 | return this.value(ast.value, context); 2457 | case AST.UnaryExpression: 2458 | right = this.recurse(ast.argument); 2459 | return this["unary" + ast.operator](right, context); 2460 | case AST.BinaryExpression: 2461 | left = this.recurse(ast.left); 2462 | right = this.recurse(ast.right); 2463 | return this["binary" + ast.operator](left, right, context); 2464 | case AST.LogicalExpression: 2465 | left = this.recurse(ast.left); 2466 | right = this.recurse(ast.right); 2467 | return this["binary" + ast.operator](left, right, context); 2468 | case AST.ConditionalExpression: 2469 | return this["ternary?:"]( 2470 | this.recurse(ast.test), 2471 | this.recurse(ast.alternate), 2472 | this.recurse(ast.consequent), 2473 | context 2474 | ); 2475 | case AST.Identifier: 2476 | return self.identifier(ast.name, context, create); 2477 | case AST.MemberExpression: 2478 | left = this.recurse(ast.object, false, !!create); 2479 | if (!ast.computed) { 2480 | right = ast.property.name; 2481 | } 2482 | if (ast.computed) { 2483 | right = this.recurse(ast.property); 2484 | } 2485 | 2486 | return ast.computed 2487 | ? this.computedMember(left, right, context, create) 2488 | : this.nonComputedMember(left, right, context, create); 2489 | case AST.CallExpression: 2490 | args = []; 2491 | forEach(ast.arguments, function (expr) { 2492 | args.push(self.recurse(expr)); 2493 | }); 2494 | if (ast.filter) { 2495 | right = this.$filter(ast.callee.name); 2496 | } 2497 | if (!ast.filter) { 2498 | right = this.recurse(ast.callee, true); 2499 | } 2500 | return ast.filter 2501 | ? function (scope, locals, assign, inputs) { 2502 | var values = []; 2503 | for (var i = 0; i < args.length; ++i) { 2504 | values.push(args[i](scope, locals, assign, inputs)); 2505 | } 2506 | var value = right.apply(undefined, values, inputs); 2507 | return context 2508 | ? { context: undefined, name: undefined, value: value } 2509 | : value; 2510 | } 2511 | : function (scope, locals, assign, inputs) { 2512 | var rhs = right(scope, locals, assign, inputs); 2513 | var value; 2514 | if (rhs.value != null) { 2515 | var values = []; 2516 | for (var i = 0; i < args.length; ++i) { 2517 | values.push(args[i](scope, locals, assign, inputs)); 2518 | } 2519 | value = rhs.value.apply(rhs.context, values); 2520 | } 2521 | return context ? { value: value } : value; 2522 | }; 2523 | case AST.AssignmentExpression: 2524 | left = this.recurse(ast.left, true, 1); 2525 | right = this.recurse(ast.right); 2526 | return function (scope, locals, assign, inputs) { 2527 | var lhs = left(scope, false, assign, inputs); 2528 | var rhs = right(scope, locals, assign, inputs); 2529 | lhs.context[lhs.name] = rhs; 2530 | return context ? { value: rhs } : rhs; 2531 | }; 2532 | case AST.ArrayExpression: 2533 | args = []; 2534 | forEach(ast.elements, function (expr) { 2535 | args.push(self.recurse(expr)); 2536 | }); 2537 | return function (scope, locals, assign, inputs) { 2538 | var value = []; 2539 | for (var i = 0; i < args.length; ++i) { 2540 | value.push(args[i](scope, locals, assign, inputs)); 2541 | } 2542 | return context ? { value: value } : value; 2543 | }; 2544 | case AST.ObjectExpression: 2545 | args = []; 2546 | forEach(ast.properties, function (property) { 2547 | if (property.computed) { 2548 | args.push({ 2549 | key: self.recurse(property.key), 2550 | computed: true, 2551 | value: self.recurse(property.value), 2552 | }); 2553 | } else { 2554 | args.push({ 2555 | key: 2556 | property.key.type === AST.Identifier 2557 | ? property.key.name 2558 | : "" + property.key.value, 2559 | computed: false, 2560 | value: self.recurse(property.value), 2561 | }); 2562 | } 2563 | }); 2564 | return function (scope, locals, assign, inputs) { 2565 | var value = {}; 2566 | for (var i = 0; i < args.length; ++i) { 2567 | if (args[i].computed) { 2568 | value[args[i].key(scope, locals, assign, inputs)] = args[i].value( 2569 | scope, 2570 | locals, 2571 | assign, 2572 | inputs 2573 | ); 2574 | } else { 2575 | value[args[i].key] = args[i].value(scope, locals, assign, inputs); 2576 | } 2577 | } 2578 | return context ? { value: value } : value; 2579 | }; 2580 | case AST.ThisExpression: 2581 | return function (scope) { 2582 | return context ? { value: scope } : scope; 2583 | }; 2584 | case AST.LocalsExpression: 2585 | return function (scope, locals) { 2586 | return context ? { value: locals } : locals; 2587 | }; 2588 | case AST.NGValueParameter: 2589 | return function (scope, locals, assign) { 2590 | return context ? { value: assign } : assign; 2591 | }; 2592 | } 2593 | }, 2594 | 2595 | "unary+": function (argument, context) { 2596 | return function (scope, locals, assign, inputs) { 2597 | var arg = argument(scope, locals, assign, inputs); 2598 | if (isDefined(arg)) { 2599 | arg = +arg; 2600 | } else { 2601 | arg = 0; 2602 | } 2603 | return context ? { value: arg } : arg; 2604 | }; 2605 | }, 2606 | "unary-": function (argument, context) { 2607 | return function (scope, locals, assign, inputs) { 2608 | var arg = argument(scope, locals, assign, inputs); 2609 | if (isDefined(arg)) { 2610 | arg = -arg; 2611 | } else { 2612 | arg = -0; 2613 | } 2614 | return context ? { value: arg } : arg; 2615 | }; 2616 | }, 2617 | "unary!": function (argument, context) { 2618 | return function (scope, locals, assign, inputs) { 2619 | var arg = !argument(scope, locals, assign, inputs); 2620 | return context ? { value: arg } : arg; 2621 | }; 2622 | }, 2623 | "binary+": function (left, right, context) { 2624 | return function (scope, locals, assign, inputs) { 2625 | var lhs = left(scope, locals, assign, inputs); 2626 | var rhs = right(scope, locals, assign, inputs); 2627 | var arg = plusFn(lhs, rhs); 2628 | return context ? { value: arg } : arg; 2629 | }; 2630 | }, 2631 | "binary-": function (left, right, context) { 2632 | return function (scope, locals, assign, inputs) { 2633 | var lhs = left(scope, locals, assign, inputs); 2634 | var rhs = right(scope, locals, assign, inputs); 2635 | var arg = (isDefined(lhs) ? lhs : 0) - (isDefined(rhs) ? rhs : 0); 2636 | return context ? { value: arg } : arg; 2637 | }; 2638 | }, 2639 | "binary*": function (left, right, context) { 2640 | return function (scope, locals, assign, inputs) { 2641 | var arg = 2642 | left(scope, locals, assign, inputs) * 2643 | right(scope, locals, assign, inputs); 2644 | return context ? { value: arg } : arg; 2645 | }; 2646 | }, 2647 | "binary/": function (left, right, context) { 2648 | return function (scope, locals, assign, inputs) { 2649 | var arg = 2650 | left(scope, locals, assign, inputs) / 2651 | right(scope, locals, assign, inputs); 2652 | return context ? { value: arg } : arg; 2653 | }; 2654 | }, 2655 | "binary%": function (left, right, context) { 2656 | return function (scope, locals, assign, inputs) { 2657 | var arg = 2658 | left(scope, locals, assign, inputs) % 2659 | right(scope, locals, assign, inputs); 2660 | return context ? { value: arg } : arg; 2661 | }; 2662 | }, 2663 | "binary===": function (left, right, context) { 2664 | return function (scope, locals, assign, inputs) { 2665 | var arg = 2666 | left(scope, locals, assign, inputs) === 2667 | right(scope, locals, assign, inputs); 2668 | return context ? { value: arg } : arg; 2669 | }; 2670 | }, 2671 | "binary!==": function (left, right, context) { 2672 | return function (scope, locals, assign, inputs) { 2673 | var arg = 2674 | left(scope, locals, assign, inputs) !== 2675 | right(scope, locals, assign, inputs); 2676 | return context ? { value: arg } : arg; 2677 | }; 2678 | }, 2679 | "binary==": function (left, right, context) { 2680 | return function (scope, locals, assign, inputs) { 2681 | var arg = 2682 | left(scope, locals, assign, inputs) == 2683 | right(scope, locals, assign, inputs); 2684 | return context ? { value: arg } : arg; 2685 | }; 2686 | }, 2687 | "binary!=": function (left, right, context) { 2688 | return function (scope, locals, assign, inputs) { 2689 | var arg = 2690 | left(scope, locals, assign, inputs) != 2691 | right(scope, locals, assign, inputs); 2692 | return context ? { value: arg } : arg; 2693 | }; 2694 | }, 2695 | "binary<": function (left, right, context) { 2696 | return function (scope, locals, assign, inputs) { 2697 | var arg = 2698 | left(scope, locals, assign, inputs) < 2699 | right(scope, locals, assign, inputs); 2700 | return context ? { value: arg } : arg; 2701 | }; 2702 | }, 2703 | "binary>": function (left, right, context) { 2704 | return function (scope, locals, assign, inputs) { 2705 | var arg = 2706 | left(scope, locals, assign, inputs) > 2707 | right(scope, locals, assign, inputs); 2708 | return context ? { value: arg } : arg; 2709 | }; 2710 | }, 2711 | "binary<=": function (left, right, context) { 2712 | return function (scope, locals, assign, inputs) { 2713 | var arg = 2714 | left(scope, locals, assign, inputs) <= 2715 | right(scope, locals, assign, inputs); 2716 | return context ? { value: arg } : arg; 2717 | }; 2718 | }, 2719 | "binary>=": function (left, right, context) { 2720 | return function (scope, locals, assign, inputs) { 2721 | var arg = 2722 | left(scope, locals, assign, inputs) >= 2723 | right(scope, locals, assign, inputs); 2724 | return context ? { value: arg } : arg; 2725 | }; 2726 | }, 2727 | "binary&&": function (left, right, context) { 2728 | return function (scope, locals, assign, inputs) { 2729 | var arg = 2730 | left(scope, locals, assign, inputs) && 2731 | right(scope, locals, assign, inputs); 2732 | return context ? { value: arg } : arg; 2733 | }; 2734 | }, 2735 | "binary||": function (left, right, context) { 2736 | return function (scope, locals, assign, inputs) { 2737 | var arg = 2738 | left(scope, locals, assign, inputs) || 2739 | right(scope, locals, assign, inputs); 2740 | return context ? { value: arg } : arg; 2741 | }; 2742 | }, 2743 | "ternary?:": function (test, alternate, consequent, context) { 2744 | return function (scope, locals, assign, inputs) { 2745 | var arg = test(scope, locals, assign, inputs) 2746 | ? alternate(scope, locals, assign, inputs) 2747 | : consequent(scope, locals, assign, inputs); 2748 | return context ? { value: arg } : arg; 2749 | }; 2750 | }, 2751 | value: function (value, context) { 2752 | return function () { 2753 | return context 2754 | ? { context: undefined, name: undefined, value: value } 2755 | : value; 2756 | }; 2757 | }, 2758 | identifier: function (name, context, create) { 2759 | return function (scope, locals) { 2760 | var base = locals && name in locals ? locals : scope; 2761 | if (create && create !== 1 && base && base[name] == null) { 2762 | base[name] = {}; 2763 | } 2764 | var value; 2765 | if (base && hasOwnProperty.call(base, name)) { 2766 | value = base ? base[name] : undefined; 2767 | } 2768 | if (context) { 2769 | return { context: base, name: name, value: value }; 2770 | } 2771 | return value; 2772 | }; 2773 | }, 2774 | computedMember: function (left, right, context, create) { 2775 | return function (scope, locals, assign, inputs) { 2776 | var lhs = left(scope, locals, assign, inputs); 2777 | var rhs; 2778 | var value; 2779 | if (lhs != null) { 2780 | rhs = right(scope, locals, assign, inputs); 2781 | rhs = getStringValue(rhs); 2782 | if (create && create !== 1) { 2783 | if (lhs && !lhs[rhs]) { 2784 | lhs[rhs] = {}; 2785 | } 2786 | } 2787 | if (Object.prototype.hasOwnProperty.call(lhs, rhs)) { 2788 | value = lhs[rhs]; 2789 | } 2790 | } 2791 | if (context) { 2792 | return { context: lhs, name: rhs, value: value }; 2793 | } 2794 | return value; 2795 | }; 2796 | }, 2797 | nonComputedMember: function (left, right, context, create) { 2798 | return function (scope, locals, assign, inputs) { 2799 | var lhs = left(scope, locals, assign, inputs); 2800 | if (create && create !== 1) { 2801 | if (lhs && lhs[right] == null) { 2802 | lhs[right] = {}; 2803 | } 2804 | } 2805 | var value = undefined; 2806 | if (lhs != null && Object.prototype.hasOwnProperty.call(lhs, right)) { 2807 | value = lhs[right]; 2808 | } 2809 | 2810 | if (context) { 2811 | return { context: lhs, name: right, value: value }; 2812 | } 2813 | return value; 2814 | }; 2815 | }, 2816 | inputs: function (input, watchId) { 2817 | return function (scope, value, locals, inputs) { 2818 | if (inputs) { 2819 | return inputs[watchId]; 2820 | } 2821 | return input(scope, value, locals); 2822 | }; 2823 | }, 2824 | }; 2825 | 2826 | /** 2827 | * @constructor 2828 | */ 2829 | var Parser = function Parser(lexer, $filter, options) { 2830 | this.lexer = lexer; 2831 | this.$filter = $filter; 2832 | options = options || {}; 2833 | options.handleThis = options.handleThis != null ? options.handleThis : true; 2834 | this.options = options; 2835 | this.ast = new AST(lexer, options); 2836 | this.ast.selfReferential = { 2837 | $locals: { type: AST.LocalsExpression }, 2838 | }; 2839 | if (options.handleThis) { 2840 | this.ast.selfReferential.this = { type: AST.ThisExpression }; 2841 | } 2842 | this.astCompiler = options.csp 2843 | ? new ASTInterpreter(this.ast, $filter) 2844 | : new ASTCompiler(this.ast, $filter); 2845 | }; 2846 | 2847 | Parser.prototype = { 2848 | constructor: Parser, 2849 | 2850 | parse: function (text) { 2851 | return this.astCompiler.compile(text); 2852 | }, 2853 | }; 2854 | 2855 | exports.Lexer = Lexer; 2856 | exports.Parser = Parser; 2857 | -------------------------------------------------------------------------------- /lib/sync: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cat <
parse.js 4 | /* remove eslint errors to see if there is something really wrong */ 5 | /*eslint quotes: [0]*/ 6 | /*eslint indent: [0]*/ 7 | /*eslint vars-on-top: [0]*/ 8 | /*eslint yoda: 0*/ 9 | /*eslint curly: 0*/ 10 | /*eslint no-implicit-coercion: 0*/ 11 | /*eslint newline-after-var: 0*/ 12 | /*eslint space-before-function-paren: 0*/ 13 | /*eslint block-spacing: 0*/ 14 | /*eslint brace-style: 0*/ 15 | /*eslint complexity: 0*/ 16 | /*eslint one-var: 0*/ 17 | /*eslint eqeqeq: 0*/ 18 | /*eslint object-curly-spacing: 0*/ 19 | /*eslint quote-props: 0*/ 20 | /*eslint key-spacing: 0*/ 21 | /*eslint valid-jsdoc: 0*/ 22 | /*eslint func-style: 0*/ 23 | /*eslint no-nested-ternary: 0*/ 24 | /*eslint operator-linebreak: 0*/ 25 | /*eslint no-multi-spaces: 0*/ 26 | /*eslint no-constant-condition: 0*/ 27 | /*eslint comma-spacing: 0*/ 28 | /*eslint no-else-return: 0*/ 29 | /*eslint no-warning-comments: 0*/ 30 | /*eslint default-case: 0*/ 31 | /*eslint consistent-return: 0*/ 32 | /*eslint no-undefined: 0*/ 33 | /*eslint no-new-func: 0*/ 34 | /*eslint max-nested-callbacks: 0*/ 35 | /*eslint padded-blocks: 0*/ 36 | /*eslint no-self-compare: 0*/ 37 | /*eslint no-multiple-empty-lines: 0*/ 38 | /*eslint no-new: 0*/ 39 | /*eslint no-unused-vars: 0*/ 40 | 'use strict'; 41 | 42 | var window = {document: {}}; 43 | 44 | HEADER 45 | 46 | curl https://raw.githubusercontent.com/angular/angular.js/master/src/Angular.js >> parse.js 47 | curl https://raw.githubusercontent.com/angular/angular.js/master/src/stringify.js >> parse.js 48 | curl https://raw.githubusercontent.com/angular/angular.js/master/src/minErr.js >> parse.js 49 | curl https://raw.githubusercontent.com/angular/angular.js/master/src/ng/parse.js >> parse.js 50 | 51 | cat <
> parse.js 52 | 53 | exports.Lexer = Lexer; 54 | exports.Parser = Parser; 55 | 56 | 57 | HEADER 58 | 59 | # now we are going to expose the AST 60 | 61 | search='fn.literal = isLiteral(ast);' # sample input containing metachars. 62 | searchEscaped=$(sed 's/[^^]/[&]/g; s/\^/\\^/g' <<< "$search") # escape it. 63 | sed -i "s/${searchEscaped}/fn.ast = ast;\n${search}/g" parse.js 64 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-expressions", 3 | "version": "1.4.3", 4 | "description": "Angular expressions as standalone module", 5 | "main": "./lib/main.js", 6 | "scripts": { 7 | "es-check": "es-check es5 lib/*.js", 8 | "preversion": "npm run es-check && npm run lint && npm run test && npm run test:typings && attw . --pack", 9 | "test": "mocha test/main.test.js -R spec", 10 | "test:typings": "tsd .", 11 | "lint": "eslint lib test && prettier lib/*.js test/*.js lib/*.ts *.md --list-different", 12 | "lint:fix": "eslint --fix lib test && prettier --write lib/*.js test/*.js *.md lib/*.ts" 13 | }, 14 | "keywords": [ 15 | "angular", 16 | "expression", 17 | "parser", 18 | "lexer", 19 | "parse", 20 | "eval", 21 | "source" 22 | ], 23 | "devDependencies": { 24 | "@arethetypeswrong/cli": "^0.18.1", 25 | "chai": "^5.2.0", 26 | "es-check": "^9.1.4", 27 | "eslint": "^9.28.0", 28 | "globals": "^16.2.0", 29 | "mocha": "^11.5.0", 30 | "prettier": "^3.5.3", 31 | "tsd": "^0.32.0", 32 | "webpack": "^5.99.9", 33 | "webpack-cli": "^6.0.1", 34 | "webpack-dev-server": "^5.2.2" 35 | }, 36 | "repository": { 37 | "type": "git", 38 | "url": "https://github.com/peerigon/angular-expressions.git" 39 | }, 40 | "testling": { 41 | "harness": "mocha", 42 | "files": "test/main.js", 43 | "browsers": [ 44 | "ie/8..latest", 45 | "chrome/27..latest", 46 | "firefox/22..latest", 47 | "safari/latest", 48 | "opera/latest", 49 | "iphone/latest", 50 | "ipad/latest", 51 | "android-browser/latest" 52 | ] 53 | }, 54 | "author": "peerigon ", 55 | "types": "./lib/main.d.ts", 56 | "license": "Unlicense" 57 | } 58 | -------------------------------------------------------------------------------- /test/main.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var chai = require("chai"); 4 | var expect = chai.expect; 5 | var expressions = require("../lib/main.js"); 6 | var compile = expressions.compile; 7 | 8 | function resolveSoon(val) { 9 | return new Promise(function (resolve) { 10 | setTimeout(function () { 11 | resolve(val); 12 | }, 1); 13 | }); 14 | } 15 | 16 | chai.config.includeStack = true; 17 | 18 | // These tests make no claim to be complete. We only test the most important parts of angular expressions. 19 | // I hope they have their own tests ;) 20 | describe("expressions", function () { 21 | describe(".Lexer", function () { 22 | it("should be a function", function () { 23 | expect(expressions.Lexer).to.be.a("function"); 24 | }); 25 | 26 | it("should provide a .lex()-method", function () { 27 | var lexer = new expressions.Lexer(); 28 | 29 | expect(lexer.lex).to.be.a("function"); 30 | }); 31 | }); 32 | 33 | describe(".Parser", function () { 34 | it("should be a function", function () { 35 | expect(expressions.Parser).to.be.a("function"); 36 | }); 37 | 38 | it("should provide a .parse()-method", function () { 39 | var parser = new expressions.Parser(undefined, undefined, {}); 40 | 41 | expect(parser.parse).to.be.a("function"); 42 | }); 43 | 44 | it("should work with Parser and use handleThis by default", function () { 45 | expect( 46 | new expressions.Parser(new expressions.Lexer(), null, { 47 | csp: true, 48 | }).parse("this+this")(2) 49 | ).to.equal(4); 50 | }); 51 | }); 52 | 53 | describe(".compile(src)", function () { 54 | var scope; 55 | var evaluate; 56 | 57 | beforeEach(function () { 58 | scope = { 59 | ship: { 60 | pirate: { 61 | name: "Jenny", 62 | }, 63 | }, 64 | }; 65 | }); 66 | 67 | it("should return a function", function () { 68 | expect(compile("")).to.be.a("function"); 69 | }); 70 | 71 | it("should throw an error if the given value is not a string", function () { 72 | expect(function () { 73 | compile(); 74 | }).to.throw("src must be a string, instead saw 'undefined'"); 75 | }); 76 | 77 | it("should expose the ast", function () { 78 | expect(compile("tmp").ast).to.be.a("object"); 79 | }); 80 | 81 | describe("when evaluating literals", function () { 82 | it("should return null", function () { 83 | evaluate = compile("null"); 84 | expect(evaluate(scope)).to.equal(null); 85 | }); 86 | 87 | it("should return true", function () { 88 | evaluate = compile("true"); 89 | expect(evaluate(scope)).to.equal(true); 90 | }); 91 | 92 | it("should return false", function () { 93 | evaluate = compile("false"); 94 | expect(evaluate(scope)).to.equal(false); 95 | }); 96 | 97 | it("should return 2.34e5", function () { 98 | evaluate = compile("2.34e5"); 99 | expect(evaluate(scope)).to.equal(2.34e5); 100 | }); 101 | 102 | it("should return 'string'", function () { 103 | evaluate = compile("'string'"); 104 | expect(evaluate(scope)).to.equal("string"); 105 | }); 106 | 107 | it("should return `string` with backticks", function () { 108 | evaluate = compile("`string`"); 109 | expect(evaluate(scope)).to.equal("string"); 110 | }); 111 | 112 | it("should return `string` with backticks and quotes", function () { 113 | evaluate = compile("`string'' \"string`"); 114 | expect(evaluate(scope)).to.equal("string'' \"string"); 115 | }); 116 | 117 | it("should work with `string` with new lines", function () { 118 | evaluate = compile("`string\n`"); 119 | expect(evaluate(scope)).to.equal("string\n"); 120 | }); 121 | 122 | it("should work with variable substitutions `Hello ${name}`", function () { 123 | evaluate = compile("`Hello ${name}, what's up ?`"); 124 | expect(evaluate({ name: "John" })).to.equal("Hello John, what's up ?"); 125 | }); 126 | 127 | it("should work with variable substitutions `Hello ${3*3}`", function () { 128 | evaluate = compile("`Hello ${3*3}, what's up ?`"); 129 | expect(evaluate({ name: "John" })).to.equal("Hello 9, what's up ?"); 130 | }); 131 | 132 | it("should work with multiple variable substitutions", function () { 133 | evaluate = compile("`User : ${user}, Age : ${age}`"); 134 | expect(evaluate({ user: "John", age: 55 })).to.equal( 135 | "User : John, Age : 55" 136 | ); 137 | }); 138 | 139 | it("should return [ship, 1, 2, []]", function () { 140 | evaluate = compile("[ship, 1, 2, []]"); 141 | expect(evaluate(scope)).to.eql([scope.ship, 1, 2, []]); 142 | }); 143 | 144 | it("should return { test: 'value', 'new-object': {} }", function () { 145 | evaluate = compile("{ test: 'value', 'new-object': {} }"); 146 | expect(evaluate(scope)).to.eql({ test: "value", "new-object": {} }); 147 | }); 148 | 149 | it("should return context value when nothing in the scope", function () { 150 | evaluate = compile("test"); 151 | expect(evaluate(scope, { test: "hello" })).to.equal("hello"); 152 | }); 153 | 154 | it("should return context value when something in the scope", function () { 155 | evaluate = compile("test"); 156 | expect(evaluate({ test: "bye" }, { test: "hello" })).to.equal("hello"); 157 | }); 158 | }); 159 | 160 | describe("when evaluating simple key look-ups", function () { 161 | it("should return the value if its defined on scope", function () { 162 | evaluate = compile("ship"); 163 | expect(evaluate(scope)).to.equal(scope.ship); 164 | }); 165 | 166 | it("should return undefined instead of throwing a ReferenceError if it's not defined on scope", function () { 167 | evaluate = compile("notDefined"); 168 | expect(evaluate(scope)).to.equal(undefined); 169 | }); 170 | 171 | it("should return null instead of undefined is the data contains null", function () { 172 | evaluate = compile("someVal"); 173 | expect(evaluate({ someVal: null })).to.equal(null); 174 | }); 175 | 176 | it("should return the scope even when the 'this' keyword is used", function () { 177 | evaluate = compile("this"); 178 | expect(evaluate(scope)).to.equal(scope); 179 | }); 180 | 181 | it("should be possible to access the 'this' key if setting the handleThis config to false", function () { 182 | expect( 183 | compile("this + this", { handleThis: false })({ this: 2 }) 184 | ).to.equal(4); 185 | expect(compile("this + this")({ this: 2 })).to.equal( 186 | "[object Object][object Object]" 187 | ); 188 | }); 189 | }); 190 | 191 | describe("when evaluating simple assignments", function () { 192 | it("should set the new value on scope", function () { 193 | evaluate = compile("newValue = 'new'"); 194 | evaluate(scope); 195 | expect(scope.newValue).to.equal("new"); 196 | }); 197 | 198 | it("should change the value if its defined on scope", function () { 199 | evaluate = compile("ship = 'ship'"); 200 | evaluate(scope); 201 | expect(scope.ship).to.equal("ship"); 202 | }); 203 | }); 204 | 205 | describe("when evaluating dot-notated loop-ups", function () { 206 | it("should return the value if its defined on scope", function () { 207 | evaluate = compile("ship.pirate.name"); 208 | expect(evaluate(scope)).to.equal("Jenny"); 209 | }); 210 | 211 | it("should return undefined instead of throwing a ReferenceError if it's not defined on scope", function () { 212 | evaluate = compile("island.pirate.name"); 213 | expect(evaluate(scope)).to.equal(undefined); 214 | }); 215 | }); 216 | 217 | describe("Security", function () { 218 | it("should not leak", function () { 219 | evaluate = compile( 220 | "''['c'+'onstructor']['c'+'onstructor']('return process;')()" 221 | ); 222 | const result = evaluate({}); 223 | expect(result).to.equal(undefined); 224 | }); 225 | 226 | it("should not leak indirectly with string concatenation", function () { 227 | evaluate = compile( 228 | "a = null; a = ''['c'+'onstructor']['c'+'onstructor']; a = a('return process;'); a();" 229 | ); 230 | const result = evaluate({}); 231 | expect(result).to.equal(undefined); 232 | }); 233 | 234 | it("should not leak indirectly with string concatenation with locals", function () { 235 | evaluate = compile( 236 | "a = null; a = ''['c'+'onstructor']['c'+'onstructor']; a = a('return process;'); a();", 237 | { csp: true } 238 | ); 239 | const result = evaluate({}, {}); 240 | expect(result).to.equal(undefined); 241 | }); 242 | 243 | it("should not leak indirectly with literal string", function () { 244 | evaluate = compile( 245 | "a = null; a = ''['constructor']['constructor']; a = a('return process;'); a();" 246 | ); 247 | const result = evaluate({}); 248 | expect(result).to.equal(undefined); 249 | }); 250 | 251 | it("should not be able to rewrite hasOwnProperty", function () { 252 | const scope = { 253 | // Pre-condition: any function in scope that returns a truthy value 254 | func: function () { 255 | return "anything truthy"; 256 | }, 257 | }; 258 | const options = { 259 | // Force to use ASTInterpreter 260 | csp: true, 261 | }; 262 | const result = expressions.compile( 263 | "hasOwnProperty = func; constructor.getPrototypeOf(toString).constructor('return process')()", 264 | options 265 | )(scope, scope); 266 | expect(result).to.equal(undefined); 267 | }); 268 | }); 269 | 270 | describe("when evaluating dot-notated assignments", function () { 271 | it("should set the new value on scope", function () { 272 | evaluate = compile("island.pirate.name = 'Störtebeker'"); 273 | evaluate(scope); 274 | expect(scope.island.pirate.name).to.equal("Störtebeker"); 275 | }); 276 | 277 | it("should change the value if its defined on scope", function () { 278 | expect(scope.ship.pirate.name).to.equal("Jenny"); 279 | evaluate = compile("ship.pirate.name = 'Störtebeker'"); 280 | evaluate(scope); 281 | expect(scope.ship.pirate.name).to.equal("Störtebeker"); 282 | }); 283 | 284 | it("should change the value in the scope if its defined both in scope and in locals", function () { 285 | const scope = { a: 10, b: 5 }; 286 | evaluate = compile("b = a"); 287 | const context = { b: 2 }; 288 | evaluate(scope, context); 289 | expect(context.b).to.equal(2); 290 | expect(scope.b).to.equal(10); 291 | const res = compile("b")(scope); 292 | expect(res).to.equal(10); 293 | }); 294 | }); 295 | 296 | describe("when evaluating array look-ups", function () { 297 | beforeEach(function () { 298 | scope.ships = [{ pirate: "Jenny" }, { pirate: "Störtebeker" }]; 299 | }); 300 | 301 | it("should return the value if its defined on scope", function () { 302 | evaluate = compile("ships[1].pirate"); 303 | expect(evaluate(scope)).to.equal("Störtebeker"); 304 | }); 305 | 306 | it("should return undefined instead of throwing a ReferenceError if it's not defined on scope", function () { 307 | evaluate = compile("ships[2].pirate"); 308 | expect(evaluate(scope)).to.equal(undefined); 309 | }); 310 | }); 311 | 312 | describe("when evaluating array assignments", function () { 313 | it("should change the value if its defined on scope", function () { 314 | scope.ships = [{ pirate: "Jenny" }]; 315 | evaluate = compile("ships[0].pirate = 'Störtebeker'"); 316 | evaluate(scope); 317 | expect(scope.ships[0].pirate).to.equal("Störtebeker"); 318 | }); 319 | }); 320 | 321 | describe("when evaluating function calls", function () { 322 | describe("using no arguments", function () { 323 | it("should return the function's return value", function () { 324 | scope.findPirate = function () { 325 | return scope.ship.pirate; 326 | }; 327 | 328 | evaluate = compile("findPirate()"); 329 | expect(evaluate(scope)).to.equal(scope.ship.pirate); 330 | }); 331 | 332 | it("should call the function on the scope", function () { 333 | scope.returnThis = function () { 334 | return this; 335 | }; 336 | evaluate = compile("returnThis()"); 337 | expect(evaluate(scope)).to.equal(scope); 338 | }); 339 | 340 | it("should call the function on the object where it is defined", function () { 341 | scope.ship.returnThis = function () { 342 | return this; 343 | }; 344 | evaluate = compile("ship.returnThis()"); 345 | expect(evaluate(scope)).to.equal(scope.ship); 346 | }); 347 | }); 348 | 349 | describe("using arguments", function () { 350 | it("should parse the arguments accordingly", function () { 351 | scope.findPirate = function () { 352 | return Array.prototype.slice.call(arguments); 353 | }; 354 | evaluate = compile("findPirate(ship.pirate, 1, [2, 3])"); 355 | expect(evaluate(scope)).to.eql([scope.ship.pirate, 1, [2, 3]]); 356 | }); 357 | }); 358 | }); 359 | 360 | describe("when evaluating operators", function () { 361 | it("should return the expected result when using +", function () { 362 | evaluate = compile("1 + 1"); 363 | expect(evaluate()).to.equal(2); 364 | }); 365 | 366 | it("should return the expected result when using -", function () { 367 | evaluate = compile("1 - 1"); 368 | expect(evaluate()).to.equal(0); 369 | }); 370 | 371 | it("should return the expected result when using *", function () { 372 | evaluate = compile("2 * 2"); 373 | expect(evaluate()).to.equal(4); 374 | }); 375 | 376 | it("should return the expected result when using /", function () { 377 | evaluate = compile("4 / 2"); 378 | expect(evaluate()).to.equal(2); 379 | }); 380 | 381 | it("should return the expected result when using %", function () { 382 | evaluate = compile("3 % 2"); 383 | expect(evaluate()).to.equal(1); 384 | }); 385 | 386 | it("should return the expected result when using &&", function () { 387 | evaluate = compile("true && true"); 388 | expect(evaluate()).to.equal(true); 389 | evaluate = compile("true && false"); 390 | expect(evaluate()).to.equal(false); 391 | evaluate = compile("false && false"); 392 | expect(evaluate()).to.equal(false); 393 | }); 394 | 395 | it("should return the expected result when using ||", function () { 396 | evaluate = compile("true || true"); 397 | expect(evaluate()).to.equal(true); 398 | evaluate = compile("true || false"); 399 | expect(evaluate()).to.equal(true); 400 | evaluate = compile("false || false"); 401 | expect(evaluate()).to.equal(false); 402 | }); 403 | 404 | it("should return the expected result when using !", function () { 405 | evaluate = compile("!true"); 406 | expect(evaluate()).to.equal(false); 407 | evaluate = compile("!false"); 408 | expect(evaluate()).to.equal(true); 409 | }); 410 | 411 | /* Ooops, angular doesn't support ++. Maybe someday? 412 | it("should return the expected result when using ++", function () { 413 | scope.value = 2; 414 | evaluate = compile("value++"); 415 | expect(evaluate()).to.equal(3); 416 | expect(scope.value).to.equal(3); 417 | });*/ 418 | 419 | /* Ooops, angular doesn't support --. Maybe someday? 420 | it("should return the expected result when using --", function () { 421 | scope.value = 2; 422 | evaluate = compile("value--"); 423 | expect(evaluate()).to.equal(1); 424 | expect(scope.value).to.equal(1); 425 | });*/ 426 | 427 | it("should return the expected result when using ?", function () { 428 | evaluate = compile("true? 'it works' : false"); 429 | expect(evaluate()).to.equal("it works"); 430 | evaluate = compile("false? false : 'it works'"); 431 | expect(evaluate()).to.equal("it works"); 432 | }); 433 | }); 434 | 435 | describe("using complex expressions", function () { 436 | beforeEach(function () { 437 | scope.ships = [ 438 | { 439 | pirate: function (str) { 440 | return str; 441 | }, 442 | }, 443 | { 444 | pirate: function (str) { 445 | return str; 446 | }, 447 | }, 448 | ]; 449 | scope.index = 0; 450 | scope.pi = "pi"; 451 | scope.Jenny = "Jenny"; 452 | }); 453 | 454 | it("should still be parseable and executable", function () { 455 | evaluate = compile("ships[index][pi + 'rate'](Jenny)"); 456 | expect(evaluate(scope)).to.equal("Jenny"); 457 | }); 458 | }); 459 | 460 | describe("when evaluating syntactical errors", function () { 461 | it("should give a readable error message", function () { 462 | expect(function () { 463 | compile("'unterminated string"); 464 | }).to.throw( 465 | "Lexer Error: Unterminated quote at columns 0-20 ['unterminated string] in expression ['unterminated string]." 466 | ); 467 | }); 468 | 469 | it("should give a readable error message", function () { 470 | expect(function () { 471 | compile("3 = 4"); 472 | }).to.throw( 473 | '[$parse:lval] Trying to assign a value to a non l-value\nhttp://errors.angularjs.org/"NG_VERSION_FULL"/$parse/lval' 474 | ); 475 | }); 476 | }); 477 | 478 | describe("when using filters", function () { 479 | it("should apply the given filter", function () { 480 | expressions.filters.currency = function (input, currency, digits) { 481 | input = input.toFixed(digits); 482 | 483 | if (currency === "EUR") { 484 | return input + "€"; 485 | } 486 | return input + "$"; 487 | }; 488 | 489 | evaluate = compile("1.2345 | currency:selectedCurrency:2"); 490 | expect( 491 | evaluate({ 492 | selectedCurrency: "EUR", 493 | }) 494 | ).to.equal("1.23€"); 495 | }); 496 | 497 | it("should show error message when filter does not exist", function () { 498 | expect(function () { 499 | compile("1.2345 | xxx"); 500 | }).to.throw("Filter 'xxx' is not defined"); 501 | }); 502 | 503 | it("should work with promise", async function () { 504 | expressions.filters.sumAge = async function (input) { 505 | input = await input; 506 | return input.reduce(function (sum, { age }) { 507 | return sum + age; 508 | }, 0); 509 | }; 510 | 511 | const evaluate = compile("users | sumAge"); 512 | const res = await evaluate({ 513 | users: resolveSoon([ 514 | { 515 | age: 22, 516 | }, 517 | { 518 | age: 23, 519 | }, 520 | ]), 521 | }); 522 | expect(res).to.equal(45); 523 | }); 524 | }); 525 | 526 | describe("when using argument options.filters", function () { 527 | it("should compile and apply the given filter", function () { 528 | const evaluate = compile("1.2345 | currency:selectedCurrency:2", { 529 | filters: { 530 | currency: (input, currency, digits) => { 531 | const result = input.toFixed(digits); 532 | 533 | if (currency === "EUR") { 534 | return `${result}€`; 535 | } 536 | 537 | return `${result}$`; 538 | }, 539 | }, 540 | }); 541 | 542 | expect( 543 | evaluate({ 544 | selectedCurrency: "EUR", 545 | }) 546 | ).to.eql("1.23€"); 547 | }); 548 | 549 | it("should show error message when filter does not exist", function () { 550 | expect(function () { 551 | compile("1.2345 | xxx", { 552 | filters: {}, 553 | }); 554 | }).throws("Filter 'xxx' is not defined"); 555 | }); 556 | 557 | it("should have independent filter and cache object", function () { 558 | const filters = { 559 | toDollars: (input) => { 560 | const result = input.toFixed(2); 561 | return `${result}$`; 562 | }, 563 | }; 564 | const cache = {}; 565 | 566 | compile("1.2345 | toDollars", { 567 | filters, 568 | cache, 569 | }); 570 | 571 | expect(expressions.filters).to.not.equal(filters); 572 | expect(expressions.filters).to.not.have.property("toDollars"); 573 | expect(Object.keys(cache).length).to.equal(1); 574 | }); 575 | 576 | it("should have different outcomes for the same filter name using different filter objects with different functions", function () { 577 | const firstFilters = { 578 | transform: (tag) => tag.toUpperCase(), 579 | }; 580 | const secondFilters = { 581 | transform: (tag) => tag.toLowerCase(), 582 | }; 583 | 584 | const text = '"The Quick Fox Jumps Over The Brown Log" | transform'; 585 | const resultOne = compile(text, { filters: firstFilters }); 586 | const resultTwo = compile(text, { filters: secondFilters }); 587 | 588 | expect(resultOne(text)).to.eql( 589 | "THE QUICK FOX JUMPS OVER THE BROWN LOG" 590 | ); 591 | expect(resultTwo(text)).to.eql( 592 | "the quick fox jumps over the brown log" 593 | ); 594 | }); 595 | }); 596 | 597 | describe("when using argument options.cache", function () { 598 | it("should use passed cache object", function () { 599 | const cache = {}; 600 | 601 | expect(Object.keys(cache).length).to.equal(0); 602 | compile("5.4321 | toDollars", { 603 | filters: { 604 | toDollars: (input) => input.toFixed(2), 605 | }, 606 | cache, 607 | }); 608 | 609 | expect(Object.keys(cache).length).to.equal(1); 610 | expect(compile.cache).to.not.equal(cache); 611 | expect(compile.cache).to.not.have.property("5.4321 | toDollars"); 612 | }); 613 | }); 614 | 615 | describe("when evaluating the same expression multiple times", function () { 616 | it("should cache the generated function", function () { 617 | expect(compile("a")).to.equal(compile("a")); 618 | }); 619 | }); 620 | 621 | describe("for assigning values", function () { 622 | beforeEach(function () { 623 | scope = {}; 624 | }); 625 | 626 | it("should expose an 'assign'-function", function () { 627 | var fn = compile("a"); 628 | 629 | expect(fn.assign).to.be.a("function"); 630 | fn.assign(scope, 123); 631 | expect(scope.a).to.equal(123); 632 | }); 633 | 634 | describe("the 'assign'-function", function () { 635 | it("should work for expressions ending with brackets", function () { 636 | var fn = compile("a.b['c']"); 637 | 638 | fn.assign(scope, 123); 639 | expect(scope.a.b.c).to.equal(123); 640 | }); 641 | 642 | it("should work for expressions with brackets in the middle", function () { 643 | var fn = compile('a["b"].c'); 644 | 645 | fn.assign(scope, 123); 646 | expect(scope.a.b.c).to.equal(123); 647 | }); 648 | 649 | it("should return the result of the assignment", function () { 650 | var fn = compile('a["b"].c'); 651 | 652 | expect(fn.assign(scope, 123)).to.equal(123); 653 | }); 654 | }); 655 | }); 656 | 657 | describe(".cache", function () { 658 | it("should be an object by default", function () { 659 | expect(compile.cache).to.be.an("object"); 660 | }); 661 | 662 | it("should cache the generated function by the expression", function () { 663 | const cache = {}; 664 | var fn = compile("a", { cache }); 665 | expect(Object.values(cache)[0]).to.equal(fn); 666 | }); 667 | 668 | describe("when setting it to false", function () { 669 | it("should disable the cache", function () { 670 | compile.cache = false; 671 | expect(compile("a")).to.not.equal(compile("a")); 672 | compile.cache = Object.create(null); 673 | }); 674 | }); 675 | }); 676 | }); 677 | 678 | describe(".filters", function () { 679 | it("should be an object", function () { 680 | expect(expressions.filters).to.be.an("object"); 681 | }); 682 | }); 683 | 684 | describe(".csp", function () { 685 | it("should allow to change csp : which uses no code generation from strings", function () { 686 | const result = compile("test + 1", { 687 | csp: false, 688 | })({ 689 | test: 3, 690 | }); 691 | 692 | expect(result).to.equal(4); 693 | }); 694 | 695 | it("should return the scope even when the 'this' keyword is used", function () { 696 | const scope = "Hello scope"; 697 | const evaluate = compile("this", { 698 | csp: false, 699 | }); 700 | expect(evaluate(scope)).to.equal(scope); 701 | }); 702 | 703 | it("should be possible to use handleThis to return scope['this']", function () { 704 | const scope = { this: "myval" }; 705 | const evaluate = compile("this", { 706 | handleThis: false, 707 | csp: false, 708 | }); 709 | expect(evaluate(scope)).to.equal("myval"); 710 | }); 711 | 712 | it("should be possible to calc this+this+this", function () { 713 | const evaluate = compile("this+this+this", { 714 | csp: false, 715 | }); 716 | expect(evaluate(1)).to.equal(3); 717 | }); 718 | }); 719 | 720 | describe("Equality", function () { 721 | let evaluate; 722 | it("should work with ===", function () { 723 | evaluate = compile("a === b"); 724 | expect(evaluate({ a: true, b: true })).to.eql(true); 725 | }); 726 | it("should work with ===", function () { 727 | evaluate = compile("a === b"); 728 | expect(evaluate({ a: true, b: 1 })).to.eql(false); 729 | }); 730 | it("should work with ==", function () { 731 | evaluate = compile("a == b"); 732 | expect(evaluate({ a: true, b: true })).to.eql(true); 733 | }); 734 | it("should work with ==", function () { 735 | evaluate = compile("a == b"); 736 | expect(evaluate({ a: true, b: 1 })).to.eql(true); 737 | }); 738 | it("should work with !==", function () { 739 | evaluate = compile("a !== b"); 740 | expect(evaluate({ a: "8", b: 8 })).to.eql(true); 741 | }); 742 | it("should work with !==", function () { 743 | evaluate = compile("a !== b"); 744 | expect(evaluate({ a: true, b: true })).to.eql(false); 745 | }); 746 | it("should work with !=", function () { 747 | evaluate = compile("a != b"); 748 | expect(evaluate({ a: true, b: true })).to.eql(false); 749 | }); 750 | it("should work with !=", function () { 751 | evaluate = compile("a != b"); 752 | expect(evaluate({ a: "8", b: 8 })).to.eql(false); 753 | }); 754 | }); 755 | 756 | describe(".literals", function () { 757 | it("should be possible to change literals", function () { 758 | const result = compile("key + key", { 759 | literals: { 760 | key: "MYKEY", 761 | }, 762 | })(); 763 | expect(result).to.equal("MYKEYMYKEY"); 764 | }); 765 | }); 766 | 767 | describe("Special characters", function () { 768 | var evaluate; 769 | it("should allow to define isIdentifierStart and isIdentifierContinue", function () { 770 | function validChars(ch) { 771 | return ( 772 | (ch >= "a" && ch <= "z") || 773 | (ch >= "A" && ch <= "Z") || 774 | ch === "_" || 775 | ch === "$" || 776 | "ÀÈÌÒÙàèìòùÁÉÍÓÚáéíóúÂÊÎÔÛâêîôûÃÑÕãñõÄËÏÖÜŸäëïöüÿß".indexOf(ch) !== -1 777 | ); 778 | } 779 | evaluate = compile("être_embarassé", { 780 | isIdentifierStart: validChars, 781 | isIdentifierContinue: validChars, 782 | }); 783 | 784 | expect(evaluate({ être_embarassé: "Ping" })).to.eql("Ping"); 785 | }); 786 | }); 787 | 788 | describe("prototype", function () { 789 | var evaluate; 790 | 791 | it("should not leak", function () { 792 | evaluate = compile("''.split"); 793 | expect(evaluate({})).to.eql(undefined); 794 | }); 795 | 796 | it("should not leak with computed prop", function () { 797 | evaluate = compile("a['split']"); 798 | expect(evaluate({ a: "" })).to.eql(undefined); 799 | }); 800 | 801 | it("should allow to read string length", function () { 802 | evaluate = compile("'abc'.length"); 803 | expect(evaluate({})).to.eql(3); 804 | }); 805 | 806 | it("should allow to read users length", function () { 807 | evaluate = compile("users.length"); 808 | expect(evaluate({ users: [1, 4, 4] })).to.eql(3); 809 | }); 810 | 811 | it("should disallow from changing prototype", function () { 812 | let err; 813 | try { 814 | evaluate = compile("name.split = 10"); 815 | evaluate({ name: "hello" }); 816 | } catch (e) { 817 | err = e; 818 | } 819 | expect(err.message).to.equal( 820 | "Cannot create property 'split' on string 'hello'" 821 | ); 822 | }); 823 | 824 | it("should not show value of __proto__", function () { 825 | evaluate = compile("__proto__"); 826 | expect(evaluate({})).to.eql(undefined); 827 | }); 828 | 829 | it("should not show value of __proto__ if passing context (second argument) with csp = false", function () { 830 | evaluate = compile("__proto__"); 831 | expect(evaluate({}, {})).to.eql(undefined); 832 | }); 833 | 834 | it("should not show value of __proto__ if passing context (second argument) with csp = true", function () { 835 | evaluate = compile("__proto__", { 836 | csp: true, 837 | }); 838 | expect(evaluate({}, {})).to.eql(undefined); 839 | }); 840 | 841 | it("should not show value of constructor if passing context (second argument) with csp = true", function () { 842 | evaluate = compile("constructor", { 843 | csp: true, 844 | }); 845 | expect(evaluate({}, {})).to.eql(undefined); 846 | }); 847 | 848 | it("should not show value of this['__proto__'] if passing context (second argument) with csp = true", function () { 849 | evaluate = compile("this['__proto' + '__']", { 850 | csp: true, 851 | }); 852 | expect(evaluate({}, {})).to.eql(undefined); 853 | }); 854 | 855 | it("should not show value of this['__proto__'] if passing context (second argument) with csp = false", function () { 856 | evaluate = compile("this['__proto' + '__']", { 857 | csp: false, 858 | }); 859 | expect(evaluate({}, {})).to.eql(undefined); 860 | }); 861 | 862 | it("should work with toString", function () { 863 | evaluate = compile("toString"); 864 | expect(evaluate({ toString: 10 })).to.eql(10); 865 | }); 866 | }); 867 | 868 | describe("Semicolon support", function () { 869 | var evaluate; 870 | 871 | it("should work with a;b", function () { 872 | evaluate = compile("a;b"); 873 | expect(evaluate({ a: 10, b: 5 })).to.eql(5); 874 | }); 875 | 876 | it("should work with assignment and semicolon", function () { 877 | evaluate = compile("a = a + 1; a"); 878 | expect(evaluate({ a: 0 })).to.eql(1); 879 | expect(evaluate({ a: 2 })).to.eql(3); 880 | }); 881 | }); 882 | }); 883 | --------------------------------------------------------------------------------