Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
fix(HtmlLexer): don't parse html in interpolation
fixes #5679
relates to #9340
  • Loading branch information
vicb committed Jun 22, 2016
commit 1128784e04a3b73f2daf6e71159c8fcf29eed58a
7 changes: 4 additions & 3 deletions modules/@angular/compiler/src/assertions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,10 @@ export function assertArrayOfStrings(identifier: string, value: any) {
}

const INTERPOLATION_BLACKLIST_REGEXPS = [
/^\s*$/g, // empty
/[<>]/g, // html tag
/^[\{\}]$/g, // i18n expansion
/^\s*$/, // empty
/[<>]/, // html tag
/^[{}]$/, // i18n expansion
/&(#|[a-z])/i, // character reference
];

export function assertInterpolationSymbols(identifier: string, value: any): void {
Expand Down
54 changes: 26 additions & 28 deletions modules/@angular/compiler/src/html_lexer.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import * as chars from './chars';
import {ListWrapper} from './facade/collection';
import {NumberWrapper, StringWrapper, isBlank, isPresent} from './facade/lang';
import {HtmlTagContentType, NAMED_ENTITIES, getHtmlTagDefinition} from './html_tags';
import {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from './interpolation_config';
Expand Down Expand Up @@ -238,8 +237,12 @@ class _HtmlTokenizer {
}

private _attemptStr(chars: string): boolean {
const len = chars.length;
if (this._index + len > this._length) {
return false;
}
const initialPosition = this._savePosition();
for (var i = 0; i < chars.length; i++) {
for (var i = 0; i < len; i++) {
if (!this._attemptCharCode(StringWrapper.charCodeAt(chars, i))) {
// If attempting to parse the string fails, we want to reset the parser
// to where it was before the attempt
Expand Down Expand Up @@ -333,7 +336,7 @@ class _HtmlTokenizer {
}

private _consumeRawText(
decodeEntities: boolean, firstCharOfEnd: number, attemptEndRest: Function): HtmlToken {
decodeEntities: boolean, firstCharOfEnd: number, attemptEndRest: () => boolean): HtmlToken {
var tagCloseStart: ParseLocation;
var textStart = this._getLocation();
this._beginToken(
Expand All @@ -345,6 +348,7 @@ class _HtmlTokenizer {
break;
}
if (this._index > tagCloseStart.offset) {
// add the characters consumed by the previous if statement to the output
parts.push(this._input.substring(tagCloseStart.offset, this._index));
}
while (this._peek !== firstCharOfEnd) {
Expand Down Expand Up @@ -561,23 +565,13 @@ class _HtmlTokenizer {
let interpolation = false;

do {
const savedPos = this._savePosition();
// _attemptStr advances the position when it is true.
// To push interpolation symbols, we have to reset it.
if (this._attemptStr(this.interpolationConfig.start)) {
this._restorePosition(savedPos);
for (let i = 0; i < this.interpolationConfig.start.length; i++) {
parts.push(this._readChar(true));
}
parts.push(this.interpolationConfig.start);
interpolation = true;
} else if (this._attemptStr(this.interpolationConfig.end) && interpolation) {
this._restorePosition(savedPos);
for (let i = 0; i < this.interpolationConfig.end.length; i++) {
parts.push(this._readChar(true));
}
parts.push(this.interpolationConfig.end);
interpolation = false;
} else {
this._restorePosition(savedPos);
parts.push(this._readChar(true));
}
} while (!this._isTextEnd(interpolation));
Expand All @@ -586,20 +580,26 @@ class _HtmlTokenizer {
}

private _isTextEnd(interpolation: boolean): boolean {
if (this._peek === chars.$LT || this._peek === chars.$EOF) return true;
if ((this._peek === chars.$LT && !interpolation) || this._peek === chars.$EOF) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need a doc about this, as this is a general change in direction and get the LGTM from igor and misko.

If we want to go this route we might want to do other things as well in the future.
E.g. ignore spaces for attribute values if we know it is an expression, like in <div [prop]=a + b>.

return true;
}

if (this.tokenizeExpansionForms) {
const savedPos = this._savePosition();
if (isExpansionFormStart(this._input, this._index, this.interpolationConfig.start))
if (isExpansionFormStart(this._input, this._index, this.interpolationConfig.start)) {
// start of an expansion form
return true;
this._restorePosition(savedPos);
if (this._peek === chars.$RBRACE && !interpolation &&
(this._isInExpansionCase() || this._isInExpansionForm()))
}

if (this._peek === chars.$RBRACE && !interpolation && this._isInExpansionCase()) {
// end of and expansion case
return true;
}
}

return false;
}

private _savePosition(): number[] {
private _savePosition(): [number, number, number, number, number] {
return [this._peek, this._index, this._column, this._line, this.tokens.length];
}

Expand All @@ -609,15 +609,15 @@ class _HtmlTokenizer {
return this._input.substring(start, this._index);
}

private _restorePosition(position: number[]): void {
private _restorePosition(position: [number, number, number, number, number]): void {
this._peek = position[0];
this._index = position[1];
this._column = position[2];
this._line = position[3];
let nbTokens = position[4];
if (nbTokens < this.tokens.length) {
// remove any extra tokens
this.tokens = ListWrapper.slice(this.tokens, 0, nbTokens);
this.tokens = this.tokens.slice(0, nbTokens);
}
}

Expand Down Expand Up @@ -657,10 +657,8 @@ function isNamedEntityEnd(code: number): boolean {
}

function isExpansionFormStart(input: string, offset: number, interpolationStart: string): boolean {
const substr = input.substring(offset);
return StringWrapper.charCodeAt(substr, 0) === chars.$LBRACE &&
StringWrapper.charCodeAt(substr, 1) !== chars.$LBRACE &&
!substr.startsWith(interpolationStart);
return input.charCodeAt(offset) == chars.$LBRACE &&
input.indexOf(interpolationStart, offset) != offset;
}

function isExpansionCaseStart(peek: number): boolean {
Expand Down
7 changes: 2 additions & 5 deletions modules/@angular/compiler/test/html_lexer_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -398,13 +398,10 @@ export function main() {
]);
});

// TODO(vicb): make the lexer aware of Angular expressions
// see https://github.com/angular/angular/issues/5679
it('should parse valid start tag in interpolation', () => {
expect(tokenizeAndHumanizeParts('{{ a <b && c > d }}')).toEqual([
[HtmlTokenType.TEXT, '{{ a '], [HtmlTokenType.TAG_OPEN_START, null, 'b'],
[HtmlTokenType.ATTR_NAME, null, '&&'], [HtmlTokenType.ATTR_NAME, null, 'c'],
[HtmlTokenType.TAG_OPEN_END], [HtmlTokenType.TEXT, ' d }}'], [HtmlTokenType.EOF]
[HtmlTokenType.TEXT, '{{ a <b && c > d }}'],
[HtmlTokenType.EOF],
]);
});

Expand Down
12 changes: 12 additions & 0 deletions modules/@angular/compiler/test/metadata_resolver_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,10 @@ export function main() {
.toThrowError(`['{', '}'] contains unusable interpolation symbol.`);
expect(() => resolver.getDirectiveMetadata(ComponentWithInvalidInterpolation3))
.toThrowError(`['<%', '%>'] contains unusable interpolation symbol.`);
expect(() => resolver.getDirectiveMetadata(ComponentWithInvalidInterpolation4))
.toThrowError(`['&#', '}}'] contains unusable interpolation symbol.`);
expect(() => resolver.getDirectiveMetadata(ComponentWithInvalidInterpolation5))
.toThrowError(`['&lbrace;', '}}'] contains unusable interpolation symbol.`);
}));
});

Expand Down Expand Up @@ -173,3 +177,11 @@ class ComponentWithInvalidInterpolation2 {
@Component({selector: 'someSelector', template: '', interpolation: ['<%', '%>']})
class ComponentWithInvalidInterpolation3 {
}

@Component({selector: 'someSelector', template: '', interpolation: ['&#', '}}']})
class ComponentWithInvalidInterpolation4 {
}

@Component({selector: 'someSelector', template: '', interpolation: ['&lbrace;', '}}']})
class ComponentWithInvalidInterpolation5 {
}