-
Notifications
You must be signed in to change notification settings - Fork 27.4k
feat($interpolate): escape interpolated expressions #5628
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -41,6 +41,8 @@ var $interpolateMinErr = minErr('$interpolate'); | |
function $InterpolateProvider() { | ||
var startSymbol = '{{'; | ||
var endSymbol = '}}'; | ||
var escapedStartSymbol = '{{{{'; | ||
var escapedEndSymbol = '}}}}'; | ||
|
||
/** | ||
* @ngdoc method | ||
|
@@ -49,11 +51,15 @@ function $InterpolateProvider() { | |
* Symbol to denote start of expression in the interpolated string. Defaults to `{{`. | ||
* | ||
* @param {string=} value new value to set the starting symbol to. | ||
* @param {string=} escaped the new value to set the escaped starting symbol to. | ||
* @returns {string|self} Returns the symbol when used as getter and self if used as setter. | ||
*/ | ||
this.startSymbol = function(value){ | ||
this.startSymbol = function(value, escaped){ | ||
if (value) { | ||
startSymbol = value; | ||
if (escaped && escaped !== value) { | ||
escapedStartSymbol = '' + escaped; | ||
} | ||
return this; | ||
} else { | ||
return startSymbol; | ||
|
@@ -67,11 +73,15 @@ function $InterpolateProvider() { | |
* Symbol to denote the end of expression in the interpolated string. Defaults to `}}`. | ||
* | ||
* @param {string=} value new value to set the ending symbol to. | ||
* @param {string=} escaped the new value to set the escaped ending symbol to. | ||
* @returns {string|self} Returns the symbol when used as getter and self if used as setter. | ||
*/ | ||
this.endSymbol = function(value){ | ||
this.endSymbol = function(value, escaped){ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think you should add unit tests for setting |
||
if (value) { | ||
endSymbol = value; | ||
if (escaped && escaped !== value) { | ||
escapedEndSymbol = '' + escaped; | ||
} | ||
return this; | ||
} else { | ||
return endSymbol; | ||
|
@@ -81,7 +91,10 @@ function $InterpolateProvider() { | |
|
||
this.$get = ['$parse', '$exceptionHandler', '$sce', function($parse, $exceptionHandler, $sce) { | ||
var startSymbolLength = startSymbol.length, | ||
endSymbolLength = endSymbol.length; | ||
endSymbolLength = endSymbol.length, | ||
escapedStartLength = escapedStartSymbol.length, | ||
escapedEndLength = escapedEndSymbol.length, | ||
similarStartSymbols = escapedStartSymbol.indexOf(startSymbol) === 0; | ||
|
||
/** | ||
* @ngdoc service | ||
|
@@ -154,23 +167,55 @@ function $InterpolateProvider() { | |
hasText = false, | ||
exp, | ||
concat = [], | ||
lastValuesCache = { values: {}, results: {}}; | ||
lastValuesCache = { values: {}, results: {}}, | ||
escapedStart = -1, | ||
escapedEnd = -1, | ||
escaped = true, | ||
i = -1; | ||
|
||
while(index < textLength) { | ||
if ( ((startIndex = text.indexOf(startSymbol, index)) != -1) && | ||
((endIndex = text.indexOf(endSymbol, startIndex + startSymbolLength)) != -1) ) { | ||
if (i < 0) { | ||
i = index; | ||
} | ||
|
||
if (escaped) { | ||
if ((escapedStart = text.indexOf(escapedStartSymbol, i)) != -1) { | ||
if ((escapedEnd = text.indexOf(escapedEndSymbol, escapedStart + escapedStartLength)) | ||
!= -1) { | ||
escapedEnd += escapedEndLength; | ||
} else { | ||
escaped = false; | ||
} | ||
} else { | ||
escaped = false; | ||
} | ||
} | ||
|
||
if (((startIndex = text.indexOf(startSymbol, i)) != -1) && | ||
((endIndex = text.indexOf(endSymbol, startIndex + startSymbolLength)) != -1) && | ||
(!escaped || escapedEnd < startIndex || escapedStart > endIndex + endSymbolLength) ) { | ||
if (escapedEnd < 0 && similarStartSymbols) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this here only to solve issues like There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In general, I like the idea of taking only the expression within the most internal braces. I mean the expression in |
||
while (escapedStart >= 0 && startIndex === escapedStart && | ||
text.indexOf(startSymbol, startIndex) === startIndex) { | ||
startIndex = escapedStart + startSymbolLength; | ||
escapedStart = text.indexOf(escapedStartSymbol, startIndex + startSymbolLength); | ||
} | ||
} | ||
if (index !== startIndex) hasText = true; | ||
separators.push(text.substring(index, startIndex)); | ||
separators.push(unescape(text.substring(index, startIndex))); | ||
exp = text.substring(startIndex + startSymbolLength, endIndex); | ||
expressions.push(exp); | ||
parseFns.push($parse(exp)); | ||
index = endIndex + endSymbolLength; | ||
hasInterpolation = true; | ||
i = -1; | ||
} else if (escaped && startIndex >= 0 && escapedStart < (endIndex + endSymbolLength)) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a bit confusing. Could be just |
||
i = escapedEnd; | ||
} else { | ||
// we did not find an interpolation, so we have to add the remainder to the separators array | ||
if (index !== textLength) { | ||
hasText = true; | ||
separators.push(text.substring(index)); | ||
separators.push(unescape(text.substring(index))); | ||
} | ||
break; | ||
} | ||
|
@@ -316,6 +361,24 @@ function $InterpolateProvider() { | |
return endSymbol; | ||
}; | ||
|
||
function unescape(text) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would this need to be documented? As an internal function, given the name its use seems pretty clear |
||
var i = 0, start, end, unescaped = ""; | ||
while (i < text.length) { | ||
if (((start = text.indexOf(escapedStartSymbol, i)) != -1 && | ||
((end = text.indexOf(escapedEndSymbol, start + escapedStartLength)) != -1))) { | ||
end = end + escapedEndLength; | ||
unescaped += text.substring(i, end). | ||
replace(escapedStartSymbol, startSymbol). | ||
replace(escapedEndSymbol, endSymbol); | ||
i = end; | ||
} else { | ||
unescaped += text.substring(i); | ||
break; | ||
} | ||
} | ||
return unescaped; | ||
} | ||
|
||
return $interpolate; | ||
}]; | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -61,6 +61,36 @@ describe('$interpolate', function() { | |
})); | ||
|
||
|
||
it('should not parse escaped interpolation expressions', inject(function($interpolate, $rootScope) { | ||
$rootScope.foo = 'World'; | ||
expect($interpolate('Hello, {{{{foo}}}}!')($rootScope)).toBe('Hello, {{foo}}!'); | ||
})); | ||
|
||
|
||
it('should parse expressions before escaped expressions', inject(function($interpolate, $rootScope) { | ||
$rootScope.foo = 'World'; | ||
expect($interpolate('Hello, {{foo}}! ({{{{foo}}}})')($rootScope)).toBe('Hello, World! ({{foo}})'); | ||
})); | ||
|
||
|
||
it('should parse expressions after escaped expressions', inject(function($interpolate, $rootScope) { | ||
$rootScope.foo = 'World'; | ||
expect($interpolate('Hello, {{{{foo}}}} ({{foo}}!)')($rootScope)).toBe('Hello, {{foo}} (World!)'); | ||
})); | ||
|
||
|
||
it('should not parse naively escaped expressions', inject(function($interpolate, $rootScope) { | ||
$rootScope.abc = 'Hello'; | ||
$rootScope.def = 'world'; | ||
expect($interpolate('{{{{abc}}}}}}{{{{def}}}}')($rootScope)).toBe('{{abc}}}}{{def}}'); | ||
expect($interpolate('{{{{{{abc}}}}{{{{def}}}}')($rootScope)).toBe('{{{{abc}}{{def}}'); | ||
expect($interpolate('{{{{abc}}}}{{{{def}}}}}}')($rootScope)).toBe('{{abc}}{{def}}}}'); | ||
expect($interpolate('{{{{abc}}}}{{{{{{def}}}}')($rootScope)).toBe('{{abc}}{{{{def}}'); | ||
expect($interpolate('}}}}abc{{abc}}}}{{{{def}}}')($rootScope)).toBe('}}}}abcHello}}{{world}'); | ||
expect($interpolate('{{{{{{abc{{def}}}}}}')($rootScope)).toBe('{{{{abc{{def}}}}'); | ||
})); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These are the three obvious test cases I could think of, but there may be other valuable tests as well. The configuration options don't have any error handling or tests, but if they are given incorrect values they're pretty much expected to screw things up, and that would need to be mentioned in docs. Unless a better approach could be used. |
||
|
||
|
||
describe('interpolating in a trusted context', function() { | ||
var sce; | ||
beforeEach(function() { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
might want to log a warning or throw or something, if value === escaped or === the current escaped start symbol, and ditto for the end symbol. Or maybe not, I dunno.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think it is necessary. I don't even think you should test if this is the case. There are many cases in which this feature can be used improperly,
value === escaped
is just one of them and I see no reason to single it out...