X Tutup
Skip to content

Commit aecf681

Browse files
committed
feat(HtmlLexer): allow "<" in text tokens
fixes #5550
1 parent 3a43861 commit aecf681

File tree

2 files changed

+85
-36
lines changed

2 files changed

+85
-36
lines changed

modules/angular2/src/compiler/html_lexer.ts

Lines changed: 55 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
CONST_EXPR,
77
serializeEnum
88
} from 'angular2/src/facade/lang';
9+
import {ListWrapper} from 'angular2/src/facade/collection';
910
import {ParseLocation, ParseError, ParseSourceFile, ParseSourceSpan} from './parse_util';
1011
import {getHtmlTagDefinition, HtmlTagContentType, NAMED_ENTITIES} from './html_tags';
1112

@@ -161,7 +162,7 @@ class _HtmlTokenizer {
161162
}
162163
this._beginToken(HtmlTokenType.EOF);
163164
this._endToken([]);
164-
return new HtmlTokenizeResult(this.tokens, this.errors);
165+
return new HtmlTokenizeResult(mergeTextTokens(this.tokens), this.errors);
165166
}
166167

167168
private _getLocation(): ParseLocation {
@@ -374,21 +375,37 @@ class _HtmlTokenizer {
374375
}
375376

376377
private _consumeTagOpen(start: ParseLocation) {
377-
this._attemptUntilFn(isNotWhitespace);
378-
var nameStart = this.index;
379-
this._consumeTagOpenStart(start);
380-
var lowercaseTagName = this.inputLowercase.substring(nameStart, this.index);
381-
this._attemptUntilFn(isNotWhitespace);
382-
while (this.peek !== $SLASH && this.peek !== $GT) {
383-
this._consumeAttributeName();
378+
let savedPos = this._savePosition();
379+
let lowercaseTagName;
380+
try {
381+
this._attemptUntilFn(isNotWhitespace);
382+
var nameStart = this.index;
383+
this._consumeTagOpenStart(start);
384+
lowercaseTagName = this.inputLowercase.substring(nameStart, this.index);
384385
this._attemptUntilFn(isNotWhitespace);
385-
if (this._attemptChar($EQ)) {
386+
while (this.peek !== $SLASH && this.peek !== $GT) {
387+
this._consumeAttributeName();
388+
this._attemptUntilFn(isNotWhitespace);
389+
if (this._attemptChar($EQ)) {
390+
this._attemptUntilFn(isNotWhitespace);
391+
this._consumeAttributeValue();
392+
}
386393
this._attemptUntilFn(isNotWhitespace);
387-
this._consumeAttributeValue();
388394
}
389-
this._attemptUntilFn(isNotWhitespace);
395+
this._consumeTagOpenEnd();
396+
} catch (e) {
397+
if (e instanceof ControlFlowError) {
398+
// When the start tag is invalid, assume we want a "<"
399+
this._restorePosition(savedPos);
400+
// Back to back text tokens are merged at the end
401+
this._beginToken(HtmlTokenType.TEXT, start);
402+
this._endToken(['<']);
403+
return;
404+
}
405+
406+
throw e;
390407
}
391-
this._consumeTagOpenEnd();
408+
392409
var contentTokenType = getHtmlTagDefinition(lowercaseTagName).contentType;
393410
if (contentTokenType === HtmlTagContentType.RAW_TEXT) {
394411
this._consumeRawTextWithTagClose(lowercaseTagName, false);
@@ -470,13 +487,20 @@ class _HtmlTokenizer {
470487
this._endToken([this._processCarriageReturns(parts.join(''))]);
471488
}
472489

473-
private _savePosition(): number[] { return [this.peek, this.index, this.column, this.line]; }
490+
private _savePosition(): number[] {
491+
return [this.peek, this.index, this.column, this.line, this.tokens.length];
492+
}
474493

475494
private _restorePosition(position: number[]): void {
476495
this.peek = position[0];
477496
this.index = position[1];
478497
this.column = position[2];
479498
this.line = position[3];
499+
let nbTokens = position[4];
500+
if (nbTokens < this.tokens.length) {
501+
// remove any extra tokens
502+
this.tokens = ListWrapper.slice(this.tokens, 0, nbTokens);
503+
}
480504
}
481505
}
482506

@@ -516,3 +540,21 @@ function isAsciiLetter(code: number): boolean {
516540
function isAsciiHexDigit(code: number): boolean {
517541
return code >= $a && code <= $f || code >= $0 && code <= $9;
518542
}
543+
544+
function mergeTextTokens(srcTokens: HtmlToken[]): HtmlToken[] {
545+
let dstTokens = [];
546+
let lastDstToken: HtmlToken;
547+
for (let i = 0; i < srcTokens.length; i++) {
548+
let token = srcTokens[i];
549+
if (isPresent(lastDstToken) && lastDstToken.type == HtmlTokenType.TEXT &&
550+
token.type == HtmlTokenType.TEXT) {
551+
lastDstToken.parts[0] += token.parts[0];
552+
lastDstToken.sourceSpan.end = token.sourceSpan.end;
553+
} else {
554+
lastDstToken = token;
555+
dstTokens.push(lastDstToken);
556+
}
557+
}
558+
559+
return dstTokens;
560+
}

modules/angular2/test/compiler/html_lexer_spec.ts

Lines changed: 30 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -192,15 +192,6 @@ export function main() {
192192
]);
193193
});
194194

195-
it('should report missing name after <', () => {
196-
expect(tokenizeAndHumanizeErrors('<'))
197-
.toEqual([[HtmlTokenType.TAG_OPEN_START, 'Unexpected character "EOF"', '0:1']]);
198-
});
199-
200-
it('should report missing >', () => {
201-
expect(tokenizeAndHumanizeErrors('<name'))
202-
.toEqual([[HtmlTokenType.TAG_OPEN_START, 'Unexpected character "EOF"', '0:5']]);
203-
});
204195
});
205196

206197
describe('attributes', () => {
@@ -335,20 +326,6 @@ export function main() {
335326
]);
336327
});
337328

338-
it('should report missing value after =', () => {
339-
expect(tokenizeAndHumanizeErrors('<name a='))
340-
.toEqual([[HtmlTokenType.ATTR_VALUE, 'Unexpected character "EOF"', '0:8']]);
341-
});
342-
343-
it('should report missing end quote for \'', () => {
344-
expect(tokenizeAndHumanizeErrors('<name a=\''))
345-
.toEqual([[HtmlTokenType.ATTR_VALUE, 'Unexpected character "EOF"', '0:9']]);
346-
});
347-
348-
it('should report missing end quote for "', () => {
349-
expect(tokenizeAndHumanizeErrors('<name a="'))
350-
.toEqual([[HtmlTokenType.ATTR_VALUE, 'Unexpected character "EOF"', '0:9']]);
351-
});
352329
});
353330

354331
describe('closing tags', () => {
@@ -448,6 +425,36 @@ export function main() {
448425
expect(tokenizeAndHumanizeSourceSpans('a'))
449426
.toEqual([[HtmlTokenType.TEXT, 'a'], [HtmlTokenType.EOF, '']]);
450427
});
428+
429+
it('should allow "<" in text nodes', () => {
430+
expect(tokenizeAndHumanizeParts('{{ a < b ? c : d }}'))
431+
.toEqual([[HtmlTokenType.TEXT, '{{ a < b ? c : d }}'], [HtmlTokenType.EOF]]);
432+
433+
expect(tokenizeAndHumanizeSourceSpans('<p>a<b</p>'))
434+
.toEqual([
435+
[HtmlTokenType.TAG_OPEN_START, '<p'],
436+
[HtmlTokenType.TAG_OPEN_END, '>'],
437+
[HtmlTokenType.TEXT, 'a<b'],
438+
[HtmlTokenType.TAG_CLOSE, '</p>'],
439+
[HtmlTokenType.EOF, ''],
440+
]);
441+
});
442+
443+
// TODO(vicb): make the lexer aware of Angular expressions
444+
// see https://github.com/angular/angular/issues/5679
445+
it('should parse valid start tag in interpolation', () => {
446+
expect(tokenizeAndHumanizeParts('{{ a <b && c > d }}'))
447+
.toEqual([
448+
[HtmlTokenType.TEXT, '{{ a '],
449+
[HtmlTokenType.TAG_OPEN_START, null, 'b'],
450+
[HtmlTokenType.ATTR_NAME, null, '&&'],
451+
[HtmlTokenType.ATTR_NAME, null, 'c'],
452+
[HtmlTokenType.TAG_OPEN_END],
453+
[HtmlTokenType.TEXT, ' d }}'],
454+
[HtmlTokenType.EOF]
455+
]);
456+
});
457+
451458
});
452459

453460
describe('raw text', () => {

0 commit comments

Comments
 (0)
X Tutup