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);