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) {