From 673ccede9a1ababc4c0076094a5e8df9f023302a Mon Sep 17 00:00:00 2001 From: Vlad GURDIGA Date: Wed, 23 Oct 2013 05:55:49 +0300 Subject: [PATCH] feat($parseProvider): add the `additionalIdentChars` configuration method Allows extending of the set of character allowed in identifiers used in Angular expressions. --- src/ng/parse.js | 54 ++++++++++++++++++++++++++++++++++++++++++-- test/ng/parseSpec.js | 49 ++++++++++++++++++++++++++++++++++++++-- 2 files changed, 99 insertions(+), 4 deletions(-) diff --git a/src/ng/parse.js b/src/ng/parse.js index 1bd9b0e4d6db..554f7d22ada9 100644 --- a/src/ng/parse.js +++ b/src/ng/parse.js @@ -212,7 +212,8 @@ Lexer.prototype = { isIdent: function(ch) { return ('a' <= ch && ch <= 'z' || 'A' <= ch && ch <= 'Z' || - '_' === ch || ch === '$'); + '_' === ch || ch === '$' || + this.options.additionalIdentChars(ch)); }, isExpOperator: function(ch) { @@ -1138,7 +1139,8 @@ function $ParseProvider() { var $parseOptions = { csp: false, unwrapPromises: false, - logPromiseWarnings: true + logPromiseWarnings: true, + additionalIdentChars: noop }; @@ -1225,6 +1227,54 @@ function $ParseProvider() { }; + /** + * @ngdoc method + * @name ng.$parseProvider#additionalIdentChars + * @methodOf ng.$parseProvider + * @description + * + * Allows extending of the set of character allowed in identifiers used in Angular expressions. The + * `fn` function will be passed a character as argument and is expected to return `true` or `false`, + * based on whether that character is allowed or not. It is useful when you want to have non-English + * Unicode letters in your identifiers. + * + * Since this function will be called with every characters outside of `/[a-zA-Z_$]/`, it’s a good + * idea to keep it simple and fast. When the character set you want to add is relativelly small, a + * string and `indexOf()` (as in the example below) is preferable to a regexp and `test()`. On the + * subject of performance, it is good to take advantage of how variable scope works in JS and + * define the character set — whether a large string or large regexp — *outside* of `fn` itself (as + * in the example below), so that it doesn’t have to be created on every function call. + * + * @param {function=} fn The function that will decide whether the given character is allowed or not. + * + * @returns {function|self} Returns the current setting when used as getter and self if used as + * setter. + * + * @example + * Let’s say that you’re developing an app for some business that has a domain language + * that is just hard to translate to English, and you want to try to use natural terminology, in + * Romanian. This snippet will allow you to use Romanian charactes: + * + *
+   *   app.config(function($parseProvider) {
+   *     var romanianCharacters = 'şŞţŢîÎăĂâÂ';
+   *
+   *     $parseProvider.additionalIdentChars(function(ch) {
+   *       return romanianCharacters.indexOf(ch) > -1;
+   *     });
+   *   });
+   * 
+ */ + this.additionalIdentChars = function(fn) { + if (isFunction(fn)) { + $parseOptions.additionalIdentChars = fn; + return this; + } else { + return $parseOptions.additionalIdentChars; + } + }; + + this.$get = ['$filter', '$sniffer', '$log', function($filter, $sniffer, $log) { $parseOptions.csp = $sniffer.csp; diff --git a/test/ng/parseSpec.js b/test/ng/parseSpec.js index 7d4642d1f960..935be855c833 100644 --- a/test/ng/parseSpec.js +++ b/test/ng/parseSpec.js @@ -14,7 +14,7 @@ describe('parser', function() { beforeEach(function () { lex = function () { - var lexer = new Lexer({csp: false, unwrapPromises: false}); + var lexer = new Lexer({csp: false, unwrapPromises: false, additionalIdentChars: noop}); return lexer.lex.apply(lexer, arguments); }; }); @@ -190,12 +190,45 @@ describe('parser', function() { lex("'\\u1''bla'"); }).toThrowMinErr("$parse", "lexerr", "Lexer Error: Invalid unicode escape [\\u1''b] at column 2 in expression ['\\u1''bla']."); }); + + describe('with additionalIdentChars', function() { + beforeEach(function () { + lex = function () { + var mathCharacters = 'πΣε'; + + var lexer = new Lexer({csp: false, unwrapPromises: false, additionalIdentChars: function(ch) { + return mathCharacters.indexOf(ch) > -1; + }}); + + return lexer.lex.apply(lexer, arguments); + }; + }); + + it('correctly tokenizes identifiers containing the specified special characters', function() { + var tokens = lex(" Σ == π + ε "); + + expect(tokens.length).toEqual(5); + expect(tokens[0].text).toEqual('Σ'); + expect(tokens[1].text).toEqual('=='); + expect(tokens[2].text).toEqual('π'); + expect(tokens[3].text).toEqual('+'); + expect(tokens[4].text).toEqual('ε'); + }); + + }); }); var $filterProvider, scope; - beforeEach(module(['$filterProvider', function (filterProvider) { + beforeEach(module(['$filterProvider', '$parseProvider', function (filterProvider, parseProvider) { $filterProvider = filterProvider; + + var mathCharacters = 'Σπε'; + + parseProvider.additionalIdentChars(function(ch) { + return mathCharacters.indexOf(ch) > -1; + }); + }])); @@ -328,6 +361,18 @@ describe('parser', function() { expect(scope.$eval("'a' + 'b c'")).toEqual("ab c"); }); + it('correctly evaluates identifiers with non-English characters', function() { + scope.π = 3.14; + expect(scope.$eval("π", scope)).toEqual(scope.π); + expect(scope.$eval("Σ = π + π", scope)).toEqual(scope.π + scope.π); + expect(scope.Σ).toEqual(scope.π + scope.π); + }); + + it('should resolve deeply nested paths (important for CSP mode)', function() { + scope.a = {b: {c: {d: {e: {f: {g: {h: {i: {j: {k: {l: {m: {n: 'nooo!'}}}}}}}}}}}}}; + expect(scope.$eval("a.b.c.d.e.f.g.h.i.j.k.l.m.n", scope)).toBe('nooo!'); + }); + it('should parse filters', function() { $filterProvider.register('substring', valueFn(function(input, start, end) { return input.substring(start, end);