|
6 | 6 | CONST_EXPR, |
7 | 7 | serializeEnum |
8 | 8 | } from 'angular2/src/facade/lang'; |
| 9 | +import {ListWrapper} from 'angular2/src/facade/collection'; |
9 | 10 | import {ParseLocation, ParseError, ParseSourceFile, ParseSourceSpan} from './parse_util'; |
10 | 11 | import {getHtmlTagDefinition, HtmlTagContentType, NAMED_ENTITIES} from './html_tags'; |
11 | 12 |
|
@@ -161,7 +162,7 @@ class _HtmlTokenizer { |
161 | 162 | } |
162 | 163 | this._beginToken(HtmlTokenType.EOF); |
163 | 164 | this._endToken([]); |
164 | | - return new HtmlTokenizeResult(this.tokens, this.errors); |
| 165 | + return new HtmlTokenizeResult(mergeTextTokens(this.tokens), this.errors); |
165 | 166 | } |
166 | 167 |
|
167 | 168 | private _getLocation(): ParseLocation { |
@@ -374,21 +375,37 @@ class _HtmlTokenizer { |
374 | 375 | } |
375 | 376 |
|
376 | 377 | 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); |
384 | 385 | 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 | + } |
386 | 393 | this._attemptUntilFn(isNotWhitespace); |
387 | | - this._consumeAttributeValue(); |
388 | 394 | } |
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; |
390 | 407 | } |
391 | | - this._consumeTagOpenEnd(); |
| 408 | + |
392 | 409 | var contentTokenType = getHtmlTagDefinition(lowercaseTagName).contentType; |
393 | 410 | if (contentTokenType === HtmlTagContentType.RAW_TEXT) { |
394 | 411 | this._consumeRawTextWithTagClose(lowercaseTagName, false); |
@@ -470,13 +487,20 @@ class _HtmlTokenizer { |
470 | 487 | this._endToken([this._processCarriageReturns(parts.join(''))]); |
471 | 488 | } |
472 | 489 |
|
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 | + } |
474 | 493 |
|
475 | 494 | private _restorePosition(position: number[]): void { |
476 | 495 | this.peek = position[0]; |
477 | 496 | this.index = position[1]; |
478 | 497 | this.column = position[2]; |
479 | 498 | 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 | + } |
480 | 504 | } |
481 | 505 | } |
482 | 506 |
|
@@ -516,3 +540,21 @@ function isAsciiLetter(code: number): boolean { |
516 | 540 | function isAsciiHexDigit(code: number): boolean { |
517 | 541 | return code >= $a && code <= $f || code >= $0 && code <= $9; |
518 | 542 | } |
| 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 | +} |
0 commit comments