(attrName: AttributeNameToken)
| 639 | } |
| 640 | |
| 641 | private _consumeAttr(attrName: AttributeNameToken): html.Attribute { |
| 642 | const fullName = mergeNsAndName(attrName.parts[0], attrName.parts[1]); |
| 643 | let attrEnd = attrName.sourceSpan.end; |
| 644 | |
| 645 | // Consume any quote |
| 646 | if (this._peek.type === TokenType.ATTR_QUOTE) { |
| 647 | this._advance(); |
| 648 | } |
| 649 | |
| 650 | // Consume the attribute value |
| 651 | let value = ''; |
| 652 | const valueTokens: InterpolatedAttributeToken[] = []; |
| 653 | let valueStartSpan: ParseSourceSpan | undefined = undefined; |
| 654 | let valueEnd: ParseLocation | undefined = undefined; |
| 655 | // NOTE: We need to use a new variable `nextTokenType` here to hide the actual type of |
| 656 | // `_peek.type` from TS. Otherwise TS will narrow the type of `_peek.type` preventing it from |
| 657 | // being able to consider `ATTR_VALUE_INTERPOLATION` as an option. This is because TS is not |
| 658 | // able to see that `_advance()` will actually mutate `_peek`. |
| 659 | const nextTokenType = this._peek.type as TokenType; |
| 660 | if (nextTokenType === TokenType.ATTR_VALUE_TEXT) { |
| 661 | valueStartSpan = this._peek.sourceSpan; |
| 662 | valueEnd = this._peek.sourceSpan.end; |
| 663 | while ( |
| 664 | this._peek.type === TokenType.ATTR_VALUE_TEXT || |
| 665 | this._peek.type === TokenType.ATTR_VALUE_INTERPOLATION || |
| 666 | this._peek.type === TokenType.ENCODED_ENTITY |
| 667 | ) { |
| 668 | const valueToken = this._advance<InterpolatedAttributeToken>(); |
| 669 | valueTokens.push(valueToken); |
| 670 | if (valueToken.type === TokenType.ATTR_VALUE_INTERPOLATION) { |
| 671 | // For backward compatibility we decode HTML entities that appear in interpolation |
| 672 | // expressions. This is arguably a bug, but it could be a considerable breaking change to |
| 673 | // fix it. It should be addressed in a larger project to refactor the entire parser/lexer |
| 674 | // chain after View Engine has been removed. |
| 675 | value += valueToken.parts.join('').replace(/&([^;]+);/g, decodeEntity); |
| 676 | } else if (valueToken.type === TokenType.ENCODED_ENTITY) { |
| 677 | value += valueToken.parts[0]; |
| 678 | } else { |
| 679 | value += valueToken.parts.join(''); |
| 680 | } |
| 681 | valueEnd = attrEnd = valueToken.sourceSpan.end; |
| 682 | } |
| 683 | } |
| 684 | |
| 685 | // Consume any quote |
| 686 | if (this._peek.type === TokenType.ATTR_QUOTE) { |
| 687 | const quoteToken = this._advance<AttributeQuoteToken>(); |
| 688 | attrEnd = quoteToken.sourceSpan.end; |
| 689 | } |
| 690 | |
| 691 | const valueSpan = |
| 692 | valueStartSpan && |
| 693 | valueEnd && |
| 694 | new ParseSourceSpan(valueStartSpan.start, valueEnd, valueStartSpan.fullStart); |
| 695 | return new html.Attribute( |
| 696 | fullName, |
| 697 | value, |
| 698 | new ParseSourceSpan(attrName.sourceSpan.start, attrEnd, attrName.sourceSpan.fullStart), |
no test coverage detected