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