diff --git a/packages/compiler-cli/src/diagnostics/expression_diagnostics.ts b/packages/compiler-cli/src/diagnostics/expression_diagnostics.ts index 3b858c2468..54b026c7ce 100644 --- a/packages/compiler-cli/src/diagnostics/expression_diagnostics.ts +++ b/packages/compiler-cli/src/diagnostics/expression_diagnostics.ts @@ -272,8 +272,7 @@ class ExpressionDiagnosticsVisitor extends RecursiveTemplateAstVisitor { const path = findNode(this.info.htmlAst, ast.sourceSpan.start.offset); const last = path.tail; if (last instanceof Attribute && last.valueSpan) { - // Add 1 for the quote. - return last.valueSpan.start.offset + 1; + return last.valueSpan.start.offset; } return ast.sourceSpan.start.offset; } diff --git a/packages/compiler/src/ml_parser/lexer.ts b/packages/compiler/src/ml_parser/lexer.ts index c3c63487b5..1c92f03072 100644 --- a/packages/compiler/src/ml_parser/lexer.ts +++ b/packages/compiler/src/ml_parser/lexer.ts @@ -25,6 +25,7 @@ export enum TokenType { CDATA_START, CDATA_END, ATTR_NAME, + ATTR_QUOTE, ATTR_VALUE, DOC_TYPE, EXPANSION_FORM_START, @@ -709,23 +710,29 @@ class _Tokenizer { } private _consumeAttributeValue() { - this._beginToken(TokenType.ATTR_VALUE); let value: string; if (this._peek === chars.$SQ || this._peek === chars.$DQ) { + this._beginToken(TokenType.ATTR_QUOTE); const quoteChar = this._peek; this._advance(); + this._endToken([String.fromCodePoint(quoteChar)]); + this._beginToken(TokenType.ATTR_VALUE); const parts: string[] = []; while (this._peek !== quoteChar) { parts.push(this._readChar(true)); } value = parts.join(''); + this._endToken([this._processCarriageReturns(value)]); + this._beginToken(TokenType.ATTR_QUOTE); this._advance(); + this._endToken([String.fromCodePoint(quoteChar)]); } else { + this._beginToken(TokenType.ATTR_VALUE); const valueStart = this._index; this._requireCharCodeUntilFn(isNameEnd, 1); value = this._input.substring(valueStart, this._index); + this._endToken([this._processCarriageReturns(value)]); } - this._endToken([this._processCarriageReturns(value)]); } private _consumeTagOpenEnd() { diff --git a/packages/compiler/src/ml_parser/parser.ts b/packages/compiler/src/ml_parser/parser.ts index b74e9d00a2..845adea5a7 100644 --- a/packages/compiler/src/ml_parser/parser.ts +++ b/packages/compiler/src/ml_parser/parser.ts @@ -326,12 +326,19 @@ class _TreeBuilder { let end = attrName.sourceSpan.end; let value = ''; let valueSpan: ParseSourceSpan = undefined !; + if (this._peek.type === lex.TokenType.ATTR_QUOTE) { + this._advance(); + } if (this._peek.type === lex.TokenType.ATTR_VALUE) { const valueToken = this._advance(); value = valueToken.parts[0]; end = valueToken.sourceSpan.end; valueSpan = valueToken.sourceSpan; } + if (this._peek.type === lex.TokenType.ATTR_QUOTE) { + const quoteToken = this._advance(); + end = quoteToken.sourceSpan.end; + } return new html.Attribute( fullName, value, new ParseSourceSpan(attrName.sourceSpan.start, end), valueSpan); } diff --git a/packages/compiler/test/ml_parser/html_parser_spec.ts b/packages/compiler/test/ml_parser/html_parser_spec.ts index 38d07fa62a..573001455b 100644 --- a/packages/compiler/test/ml_parser/html_parser_spec.ts +++ b/packages/compiler/test/ml_parser/html_parser_spec.ts @@ -429,8 +429,15 @@ import {humanizeDom, humanizeDomSourceSpans, humanizeLineColumn} from './ast_spe it('should report a value span for an attribute with a value', () => { const ast = parser.parse('
', 'TestComp'); const attr = (ast.rootNodes[0] as html.Element).attrs[0]; + expect(attr.valueSpan !.start.offset).toEqual(10); + expect(attr.valueSpan !.end.offset).toEqual(12); + }); + + it('should report a value span for an unquoted attribute value', () => { + const ast = parser.parse('
', 'TestComp'); + const attr = (ast.rootNodes[0] as html.Element).attrs[0]; expect(attr.valueSpan !.start.offset).toEqual(9); - expect(attr.valueSpan !.end.offset).toEqual(13); + expect(attr.valueSpan !.end.offset).toEqual(11); }); }); diff --git a/packages/compiler/test/ml_parser/lexer_spec.ts b/packages/compiler/test/ml_parser/lexer_spec.ts index 36379c5a31..68dcf391fe 100644 --- a/packages/compiler/test/ml_parser/lexer_spec.ts +++ b/packages/compiler/test/ml_parser/lexer_spec.ts @@ -238,11 +238,17 @@ import {ParseLocation, ParseSourceFile, ParseSourceSpan} from '../../src/parse_u expect(tokenizeAndHumanizeParts('')).toEqual([ [lex.TokenType.TAG_OPEN_START, null, 't'], [lex.TokenType.ATTR_NAME, null, 'a'], + [lex.TokenType.ATTR_QUOTE, '"'], [lex.TokenType.ATTR_VALUE, '{{v}}'], + [lex.TokenType.ATTR_QUOTE, '"'], [lex.TokenType.ATTR_NAME, null, 'b'], + [lex.TokenType.ATTR_QUOTE, '"'], [lex.TokenType.ATTR_VALUE, 's{{m}}e'], + [lex.TokenType.ATTR_QUOTE, '"'], [lex.TokenType.ATTR_NAME, null, 'c'], + [lex.TokenType.ATTR_QUOTE, '"'], [lex.TokenType.ATTR_VALUE, 's{{m//c}}e'], + [lex.TokenType.ATTR_QUOTE, '"'], [lex.TokenType.TAG_OPEN_END], [lex.TokenType.EOF], ]); @@ -270,7 +276,9 @@ import {ParseLocation, ParseSourceFile, ParseSourceSpan} from '../../src/parse_u expect(tokenizeAndHumanizeParts('')).toEqual([ [lex.TokenType.TAG_OPEN_START, null, 't'], [lex.TokenType.ATTR_NAME, null, 'a'], + [lex.TokenType.ATTR_QUOTE, '\''], [lex.TokenType.ATTR_VALUE, 'b'], + [lex.TokenType.ATTR_QUOTE, '\''], [lex.TokenType.TAG_OPEN_END], [lex.TokenType.EOF], ]); @@ -280,7 +288,9 @@ import {ParseLocation, ParseSourceFile, ParseSourceSpan} from '../../src/parse_u expect(tokenizeAndHumanizeParts('')).toEqual([ [lex.TokenType.TAG_OPEN_START, null, 't'], [lex.TokenType.ATTR_NAME, null, 'a'], + [lex.TokenType.ATTR_QUOTE, '"'], [lex.TokenType.ATTR_VALUE, 'b'], + [lex.TokenType.ATTR_QUOTE, '"'], [lex.TokenType.TAG_OPEN_END], [lex.TokenType.EOF], ]); @@ -310,7 +320,9 @@ import {ParseLocation, ParseSourceFile, ParseSourceSpan} from '../../src/parse_u expect(tokenizeAndHumanizeParts('')).toEqual([ [lex.TokenType.TAG_OPEN_START, null, 't'], [lex.TokenType.ATTR_NAME, null, 'a'], + [lex.TokenType.ATTR_QUOTE, '"'], [lex.TokenType.ATTR_VALUE, 'AA'], + [lex.TokenType.ATTR_QUOTE, '"'], [lex.TokenType.TAG_OPEN_END], [lex.TokenType.EOF], ]); @@ -320,9 +332,13 @@ import {ParseLocation, ParseSourceFile, ParseSourceSpan} from '../../src/parse_u expect(tokenizeAndHumanizeParts('')).toEqual([ [lex.TokenType.TAG_OPEN_START, null, 't'], [lex.TokenType.ATTR_NAME, null, 'a'], + [lex.TokenType.ATTR_QUOTE, '"'], [lex.TokenType.ATTR_VALUE, '&'], + [lex.TokenType.ATTR_QUOTE, '"'], [lex.TokenType.ATTR_NAME, null, 'b'], + [lex.TokenType.ATTR_QUOTE, '"'], [lex.TokenType.ATTR_VALUE, 'c&&d'], + [lex.TokenType.ATTR_QUOTE, '"'], [lex.TokenType.TAG_OPEN_END], [lex.TokenType.EOF], ]); @@ -332,7 +348,9 @@ import {ParseLocation, ParseSourceFile, ParseSourceSpan} from '../../src/parse_u expect(tokenizeAndHumanizeParts('')).toEqual([ [lex.TokenType.TAG_OPEN_START, null, 't'], [lex.TokenType.ATTR_NAME, null, 'a'], + [lex.TokenType.ATTR_QUOTE, '"'], [lex.TokenType.ATTR_VALUE, 'b && c &'], + [lex.TokenType.ATTR_QUOTE, '"'], [lex.TokenType.TAG_OPEN_END], [lex.TokenType.EOF], ]); @@ -342,7 +360,9 @@ import {ParseLocation, ParseSourceFile, ParseSourceSpan} from '../../src/parse_u expect(tokenizeAndHumanizeParts('')).toEqual([ [lex.TokenType.TAG_OPEN_START, null, 't'], [lex.TokenType.ATTR_NAME, null, 'a'], + [lex.TokenType.ATTR_QUOTE, '\''], [lex.TokenType.ATTR_VALUE, 't\ne\ns\nt'], + [lex.TokenType.ATTR_QUOTE, '\''], [lex.TokenType.TAG_OPEN_END], [lex.TokenType.EOF], ]); @@ -1001,9 +1021,13 @@ import {ParseLocation, ParseSourceFile, ParseSourceSpan} from '../../src/parse_u .toEqual([ [lex.TokenType.TAG_OPEN_START, null, 't'], [lex.TokenType.ATTR_NAME, null, 'a'], + [lex.TokenType.ATTR_QUOTE, '"'], [lex.TokenType.ATTR_VALUE, 'b'], + [lex.TokenType.ATTR_QUOTE, '"'], [lex.TokenType.ATTR_NAME, null, 'c'], + [lex.TokenType.ATTR_QUOTE, '\''], [lex.TokenType.ATTR_VALUE, 'd'], + [lex.TokenType.ATTR_QUOTE, '\''], [lex.TokenType.TAG_OPEN_END], [lex.TokenType.EOF], ]); diff --git a/packages/language-service/src/completions.ts b/packages/language-service/src/completions.ts index 3412abffef..9192d9f213 100644 --- a/packages/language-service/src/completions.ts +++ b/packages/language-service/src/completions.ts @@ -316,7 +316,7 @@ class ExpressionVisitor extends NullTemplateVisitor { // find the template binding that contains the position if (!this.attr.valueSpan) return; - const valueRelativePosition = this.position - this.attr.valueSpan.start.offset - 1; + const valueRelativePosition = this.position - this.attr.valueSpan.start.offset; const bindings = templateBindingResult.templateBindings; const binding = bindings.find( @@ -401,7 +401,7 @@ class ExpressionVisitor extends NullTemplateVisitor { private get attributeValuePosition() { if (this.attr && this.attr.valueSpan) { - return this.position - this.attr.valueSpan.start.offset - 1; + return this.position - this.attr.valueSpan.start.offset; } return 0; } diff --git a/packages/language-service/src/locate_symbol.ts b/packages/language-service/src/locate_symbol.ts index c1e9a0e161..a87750716d 100644 --- a/packages/language-service/src/locate_symbol.ts +++ b/packages/language-service/src/locate_symbol.ts @@ -33,7 +33,7 @@ export function locateSymbol(info: TemplateInfo): SymbolInfo|undefined { const dinfo = diagnosticInfoFromTemplateInfo(info); const scope = getExpressionScope(dinfo, path, inEvent); if (attribute.valueSpan) { - const expressionOffset = attribute.valueSpan.start.offset + 1; + const expressionOffset = attribute.valueSpan.start.offset; const result = getExpressionSymbol( scope, ast, templatePosition - expressionOffset, info.template.query); if (result) {