Skip to content
This repository was archived by the owner on Apr 12, 2024. It is now read-only.

Commit ad29894

Browse files
committed
feat($parse): Add the ability to define the identifier characters
Add the ability to define the identifier starts and identifier continue characters
1 parent fadea2c commit ad29894

File tree

2 files changed

+117
-8
lines changed

2 files changed

+117
-8
lines changed

src/ng/parse.js

Lines changed: 86 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ Lexer.prototype = {
150150
this.readString(ch);
151151
} else if (this.isNumber(ch) || ch === '.' && this.isNumber(this.peek())) {
152152
this.readNumber();
153-
} else if (this.isIdent(ch)) {
153+
} else if (this.isIdentifierStart(this.peekMultichar())) {
154154
this.readIdent();
155155
} else if (this.is(ch, '(){}[].,;:?')) {
156156
this.tokens.push({index: this.index, text: ch});
@@ -194,12 +194,49 @@ Lexer.prototype = {
194194
ch === '\n' || ch === '\v' || ch === '\u00A0');
195195
},
196196

197-
isIdent: function(ch) {
197+
isIdentifierStart: function(ch) {
198+
return this.options.isIdentifierStart ?
199+
this.options.isIdentifierStart(ch, this.codePointAt(ch)) :
200+
this.isValidIdentifierStart(ch);
201+
},
202+
203+
isValidIdentifierStart: function(ch) {
198204
return ('a' <= ch && ch <= 'z' ||
199205
'A' <= ch && ch <= 'Z' ||
200206
'_' === ch || ch === '$');
201207
},
202208

209+
isIdentifierContinue: function(ch) {
210+
return this.options.isIdentifierContinue ?
211+
this.options.isIdentifierContinue(ch, this.codePointAt(ch)) :
212+
this.isValidIdentifierContinue(ch);
213+
},
214+
215+
isValidIdentifierContinue: function(ch, cp) {
216+
return this.isValidIdentifierStart(ch, cp) || this.isNumber(ch);
217+
},
218+
219+
codePointAt: function(ch) {
220+
if (ch.length === 1) return ch.charCodeAt(0);
221+
/*jshint bitwise: false*/
222+
return (ch.charCodeAt(0) << 10) + ch.charCodeAt(1) - 0x35FDC00;
223+
/*jshint bitwise: true*/
224+
},
225+
226+
peekMultichar: function() {
227+
var ch = this.text.charAt(this.index);
228+
var peek = this.peek();
229+
if (!peek) {
230+
return ch;
231+
}
232+
var cp1 = ch.charCodeAt(0);
233+
var cp2 = peek.charCodeAt(0);
234+
if (cp1 >= 0xD800 && cp1 <= 0xDBFF && cp2 >= 0xDC00 && cp2 <= 0xDFFF) {
235+
return ch + peek;
236+
}
237+
return ch;
238+
},
239+
203240
isExpOperator: function(ch) {
204241
return (ch === '-' || ch === '+' || this.isNumber(ch));
205242
},
@@ -248,12 +285,13 @@ Lexer.prototype = {
248285

249286
readIdent: function() {
250287
var start = this.index;
288+
this.index += this.peekMultichar().length;
251289
while (this.index < this.text.length) {
252-
var ch = this.text.charAt(this.index);
253-
if (!(this.isIdent(ch) || this.isNumber(ch))) {
290+
var ch = this.peekMultichar();
291+
if (!this.isIdentifierContinue(ch)) {
254292
break;
255293
}
256-
this.index++;
294+
this.index += ch.length;
257295
}
258296
this.tokens.push({
259297
index: start,
@@ -1183,7 +1221,13 @@ ASTCompiler.prototype = {
11831221
},
11841222

11851223
nonComputedMember: function(left, right) {
1186-
return left + '.' + right;
1224+
var SAFE_IDENTIFIER = /[$_a-zA-Z][$_a-zA-Z0-9]*/;
1225+
var UNSAFE_CHARACTERS = /[^$_a-zA-Z0-9]/g;
1226+
if (SAFE_IDENTIFIER.test(right)) {
1227+
return left + '.' + right;
1228+
} else {
1229+
return left + '["' + right.replace(UNSAFE_CHARACTERS, this.stringEscapeFn) + '"]';
1230+
}
11871231
},
11881232

11891233
computedMember: function(left, right) {
@@ -1748,6 +1792,7 @@ function $ParseProvider() {
17481792
'null': null,
17491793
'undefined': undefined
17501794
};
1795+
var identStart, identContinue;
17511796

17521797
/**
17531798
* @ngdoc method
@@ -1764,17 +1809,50 @@ function $ParseProvider() {
17641809
literals[literalName] = literalValue;
17651810
};
17661811

1812+
/**
1813+
* @ngdoc method
1814+
* @name $parseProvider#setIdentifierFns
1815+
* @description
1816+
*
1817+
* Allows defining the set of characters that are allowed in Angular expressions. The function
1818+
* `identifierStart` will get called to know if a given character is a valid character to be the
1819+
* first character for an identifier. The function `identifierContinue` will get called to know if
1820+
* a given character is a valid character to be a follow-up identifier character. The functions
1821+
* `identifierStart` and `identifierContinue` will receive as arguments the single character to be
1822+
* identifier and the character code point. These arguments will be `string` and `numeric`. Keep in
1823+
* mind that the `string` parameter can be two characters long depending on the character
1824+
* representation. It is expected for the function to return `true` or `false`, whether that
1825+
* character is allowed or not.
1826+
*
1827+
* Since this function will be called extensivelly, keep the implementation of these functions fast,
1828+
* as the performance of these functions have a direct impact on the expressions parsing speed.
1829+
*
1830+
* @param {function=} identifierStart The function that will decide whether the given character is
1831+
* a valid identifier start character.
1832+
* @param {function=} identifierContinue The function that will decide whether the given character is
1833+
* a valid identifier continue character.
1834+
*/
1835+
this.setIdentifierFns = function(identifierStart, identifierContinue) {
1836+
identStart = identifierStart;
1837+
identContinue = identifierContinue;
1838+
return this;
1839+
};
1840+
17671841
this.$get = ['$filter', function($filter) {
17681842
var noUnsafeEval = csp().noUnsafeEval;
17691843
var $parseOptions = {
17701844
csp: noUnsafeEval,
17711845
expensiveChecks: false,
1772-
literals: copy(literals)
1846+
literals: copy(literals),
1847+
isIdentifierStart: isFunction(identStart) && identStart,
1848+
isIdentifierContinue: isFunction(identContinue) && identContinue
17731849
},
17741850
$parseOptionsExpensive = {
17751851
csp: noUnsafeEval,
17761852
expensiveChecks: true,
1777-
literals: copy(literals)
1853+
literals: copy(literals),
1854+
isIdentifierStart: isFunction(identStart) && identStart,
1855+
isIdentifierContinue: isFunction(identContinue) && identContinue
17781856
};
17791857
var runningChecksEnabled = false;
17801858

test/ng/parseSpec.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,37 @@ describe('parser', function() {
9595
expect(spaces).toEqual(noSpaces);
9696
});
9797

98+
it('should use callback functions to know when an identifier is valid', function() {
99+
function getText(t) { return t.text; }
100+
var isIdentifierStart = jasmine.createSpy('start');
101+
var isIdentifierContinue = jasmine.createSpy('continue');
102+
isIdentifierStart.and.returnValue(true);
103+
var lex = new Lexer({csp: false, isIdentifierStart: isIdentifierStart, isIdentifierContinue: isIdentifierContinue});
104+
105+
isIdentifierContinue.and.returnValue(true);
106+
var tokens = lex.lex('πΣε').map(getText);
107+
expect(tokens).toEqual(['πΣε']);
108+
109+
isIdentifierContinue.and.returnValue(false);
110+
tokens = lex.lex('πΣε').map(getText);
111+
expect(tokens).toEqual(['π', 'Σ', 'ε']);
112+
});
113+
114+
it('should send the unicode characters and code points', function() {
115+
function getText(t) { return t.text; }
116+
var isIdentifierStart = jasmine.createSpy('start');
117+
var isIdentifierContinue = jasmine.createSpy('continue');
118+
isIdentifierStart.and.returnValue(true);
119+
isIdentifierContinue.and.returnValue(true);
120+
var lex = new Lexer({csp: false, isIdentifierStart: isIdentifierStart, isIdentifierContinue: isIdentifierContinue});
121+
var tokens = lex.lex('\uD801\uDC37\uD852\uDF62\uDBFF\uDFFF');
122+
expect(isIdentifierStart).toHaveBeenCalledTimes(1);
123+
expect(isIdentifierStart.calls.argsFor(0)).toEqual(['\uD801\uDC37', 0x10437]);
124+
expect(isIdentifierContinue).toHaveBeenCalledTimes(2);
125+
expect(isIdentifierContinue.calls.argsFor(0)).toEqual(['\uD852\uDF62', 0x24B62]);
126+
expect(isIdentifierContinue.calls.argsFor(1)).toEqual(['\uDBFF\uDFFF', 0x10FFFF]);
127+
});
128+
98129
it('should tokenize undefined', function() {
99130
var tokens = lex("undefined");
100131
var i = 0;

0 commit comments

Comments
 (0)