diff --git a/src/ng/parse.js b/src/ng/parse.js index ede3f24bcdf7..c59559d5197c 100644 --- a/src/ng/parse.js +++ b/src/ng/parse.js @@ -224,7 +224,8 @@ Lexer.prototype = { isIdent: function(ch) { return ('a' <= ch && ch <= 'z' || 'A' <= ch && ch <= 'Z' || - '_' === ch || ch === '$'); + '_' === ch || ch === '$' || + this.options.additionalIsIdent(ch)); }, isExpOperator: function(ch) { @@ -1120,7 +1121,8 @@ function $ParseProvider() { var $parseOptions = { csp: false, unwrapPromises: false, - logPromiseWarnings: true + logPromiseWarnings: true, + additionalIsIdent: noop }; @@ -1207,6 +1209,55 @@ function $ParseProvider() { }; + /** + * @ngdoc method + * @name ng.$parseProvider#additionalIsIdent + * @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 we don’t have to create it on every function call. + * + * @param {function=} fn The function that the the caracter will be passed to, to decide whether it’s + * allowed or not. The default is set to `noop` function which returns falsy. + * + * @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.additionalIsIdent(function(ch) {
+   *       return romanianCharacters.indexOf(ch) > -1;
+   *     });
+   *   });
+   * 
+ */ + this.additionalIsIdent = function(fn) { + if (isFunction(fn)) { + $parseOptions.additionalIsIdent = fn; + return this; + } else { + return $parseOptions.additionalIsIdent; + } + }; + + 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 c72b7e818749..bc49de534086 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, additionalIsIdent: 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 additionalIsIdent', function() { + beforeEach(function () { + lex = function () { + var mathCharacters = 'πΣε'; + + var lexer = new Lexer({csp: false, unwrapPromises: false, additionalIsIdent: 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.additionalIsIdent(function(ch) { + return mathCharacters.indexOf(ch) > -1; + }); + }])); @@ -335,6 +368,13 @@ describe('parser', function() { expect(scope.$eval("x.y.z", scope)).not.toBeDefined(); }); + 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!');