fix(compiler): properly associate source spans for implicitly closed elements (#38126)
HTML is very lenient when it comes to closing elements, so Angular's parser has rules that specify which elements are implicitly closed when closing a tag. The parser keeps track of the nesting of tag names using a stack and parsing a closing tag will pop as many elements off the stack as possible, provided that the elements can be implicitly closed. For example, consider the following templates: - `<div><br></div>`, the `<br>` is implicitly closed when parsing `</div>`, because `<br>` is a void element. - `<div><p></div>`, the `<p>` is implicitly closed when parsing `</div>`, as `<p>` is allowed to be closed by the closing of its parent element. - `<ul><li>A <li>B</ul>`, the first `<li>` is implicitly closed when parsing the second `<li>`, whereas the second `<li>` would be implicitly closed when parsing the `</ul>`. In all the cases above the parsed structure would be correct, however the source span of the closing `</div>` would incorrectly be assigned to the element that is implicitly closed. The problem was that closing an element would associate the source span with the element at the top of the stack, however this may not be the element that is actually being closed if some elements would be implicitly closed. This commit fixes the issue by assigning the end source span with the element on the stack that is actually being closed. Any implicitly closed elements that are popped off the stack will not be assigned an end source span, as the implicit closing implies that no ending element is present. Note that there is a difference between self-closed elements such as `<input/>` and implicitly closed elements such as `<input>`. The former does have an end source span (identical to its start source span) whereas the latter does not. Fixes #36118 Resolves FW-2004 PR Close #38126
This commit is contained in:
parent
8edf5ba29d
commit
1a7a7360b0
|
@ -261,8 +261,9 @@ class _TreeBuilder {
|
|||
const el = new html.Element(fullName, attrs, [], span, span, undefined);
|
||||
this._pushElement(el);
|
||||
if (selfClosing) {
|
||||
this._popElement(fullName);
|
||||
el.endSourceSpan = span;
|
||||
// Elements that are self-closed have their `endSourceSpan` set to the full span, as the
|
||||
// element start tag also represents the end tag.
|
||||
this._popElement(fullName, span);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -281,25 +282,26 @@ class _TreeBuilder {
|
|||
const fullName = this._getElementFullName(
|
||||
endTagToken.parts[0], endTagToken.parts[1], this._getParentElement());
|
||||
|
||||
if (this._getParentElement()) {
|
||||
this._getParentElement()!.endSourceSpan = endTagToken.sourceSpan;
|
||||
}
|
||||
|
||||
if (this.getTagDefinition(fullName).isVoid) {
|
||||
this.errors.push(TreeError.create(
|
||||
fullName, endTagToken.sourceSpan,
|
||||
`Void elements do not have end tags "${endTagToken.parts[1]}"`));
|
||||
} else if (!this._popElement(fullName)) {
|
||||
} else if (!this._popElement(fullName, endTagToken.sourceSpan)) {
|
||||
const errMsg = `Unexpected closing tag "${
|
||||
fullName}". It may happen when the tag has already been closed by another tag. For more info see https://www.w3.org/TR/html5/syntax.html#closing-elements-that-have-implied-end-tags`;
|
||||
this.errors.push(TreeError.create(fullName, endTagToken.sourceSpan, errMsg));
|
||||
}
|
||||
}
|
||||
|
||||
private _popElement(fullName: string): boolean {
|
||||
private _popElement(fullName: string, endSourceSpan: ParseSourceSpan): boolean {
|
||||
for (let stackIndex = this._elementStack.length - 1; stackIndex >= 0; stackIndex--) {
|
||||
const el = this._elementStack[stackIndex];
|
||||
if (el.name == fullName) {
|
||||
// Record the parse span with the element that is being closed. Any elements that are
|
||||
// removed from the element stack at this point are closed implicitly, so they won't get
|
||||
// an end source span (as there is no explicit closing element).
|
||||
el.endSourceSpan = endSourceSpan;
|
||||
|
||||
this._elementStack.splice(stackIndex, this._elementStack.length - stackIndex);
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -41,6 +41,10 @@ class _Humanizer implements html.Visitor {
|
|||
|
||||
visitElement(element: html.Element, context: any): any {
|
||||
const res = this._appendContext(element, [html.Element, element.name, this.elDepth++]);
|
||||
if (this.includeSourceSpan) {
|
||||
res.push(element.startSourceSpan?.toString() ?? null);
|
||||
res.push(element.endSourceSpan?.toString() ?? null);
|
||||
}
|
||||
this.result.push(res);
|
||||
html.visitAll(this, element.attrs);
|
||||
html.visitAll(this, element.children);
|
||||
|
|
|
@ -583,7 +583,10 @@ import {humanizeDom, humanizeDomSourceSpans, humanizeLineColumn} from './ast_spe
|
|||
expect(humanizeDomSourceSpans(parser.parse(
|
||||
'<div [prop]="v1" (e)="do()" attr="v2" noValue>\na\n</div>', 'TestComp')))
|
||||
.toEqual([
|
||||
[html.Element, 'div', 0, '<div [prop]="v1" (e)="do()" attr="v2" noValue>'],
|
||||
[
|
||||
html.Element, 'div', 0, '<div [prop]="v1" (e)="do()" attr="v2" noValue>',
|
||||
'<div [prop]="v1" (e)="do()" attr="v2" noValue>', '</div>'
|
||||
],
|
||||
[html.Attribute, '[prop]', 'v1', '[prop]="v1"'],
|
||||
[html.Attribute, '(e)', 'do()', '(e)="do()"'],
|
||||
[html.Attribute, 'attr', 'v2', 'attr="v2"'],
|
||||
|
@ -602,12 +605,61 @@ import {humanizeDom, humanizeDomSourceSpans, humanizeLineColumn} from './ast_spe
|
|||
expect(node.endSourceSpan!.end.offset).toEqual(12);
|
||||
});
|
||||
|
||||
it('should not set the end source span for void elements', () => {
|
||||
expect(humanizeDomSourceSpans(parser.parse('<div><br></div>', 'TestComp'))).toEqual([
|
||||
[html.Element, 'div', 0, '<div>', '<div>', '</div>'],
|
||||
[html.Element, 'br', 1, '<br>', '<br>', null],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should not set the end source span for multiple void elements', () => {
|
||||
expect(humanizeDomSourceSpans(parser.parse('<div><br><hr></div>', 'TestComp'))).toEqual([
|
||||
[html.Element, 'div', 0, '<div>', '<div>', '</div>'],
|
||||
[html.Element, 'br', 1, '<br>', '<br>', null],
|
||||
[html.Element, 'hr', 1, '<hr>', '<hr>', null],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should not set the end source span for standalone void elements', () => {
|
||||
expect(humanizeDomSourceSpans(parser.parse('<br>', 'TestComp'))).toEqual([
|
||||
[html.Element, 'br', 0, '<br>', '<br>', null],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should set the end source span for standalone self-closing elements', () => {
|
||||
expect(humanizeDomSourceSpans(parser.parse('<br/>', 'TestComp'))).toEqual([
|
||||
[html.Element, 'br', 0, '<br/>', '<br/>', '<br/>'],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should set the end source span for self-closing elements', () => {
|
||||
expect(humanizeDomSourceSpans(parser.parse('<div><br/></div>', 'TestComp'))).toEqual([
|
||||
[html.Element, 'div', 0, '<div>', '<div>', '</div>'],
|
||||
[html.Element, 'br', 1, '<br/>', '<br/>', '<br/>'],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should not set the end source span for elements that are implicitly closed', () => {
|
||||
expect(humanizeDomSourceSpans(parser.parse('<div><p></div>', 'TestComp'))).toEqual([
|
||||
[html.Element, 'div', 0, '<div>', '<div>', '</div>'],
|
||||
[html.Element, 'p', 1, '<p>', '<p>', null],
|
||||
]);
|
||||
expect(humanizeDomSourceSpans(parser.parse('<div><li>A<li>B</div>', 'TestComp')))
|
||||
.toEqual([
|
||||
[html.Element, 'div', 0, '<div>', '<div>', '</div>'],
|
||||
[html.Element, 'li', 1, '<li>', '<li>', null],
|
||||
[html.Text, 'A', 2, 'A'],
|
||||
[html.Element, 'li', 1, '<li>', '<li>', null],
|
||||
[html.Text, 'B', 2, 'B'],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should support expansion form', () => {
|
||||
expect(humanizeDomSourceSpans(parser.parse(
|
||||
'<div>{count, plural, =0 {msg}}</div>', 'TestComp',
|
||||
{tokenizeExpansionForms: true})))
|
||||
.toEqual([
|
||||
[html.Element, 'div', 0, '<div>'],
|
||||
[html.Element, 'div', 0, '<div>', '<div>', '</div>'],
|
||||
[html.Expansion, 'count', 'plural', 1, '{count, plural, =0 {msg}}'],
|
||||
[html.ExpansionCase, '=0', 2, '=0 {msg}'],
|
||||
]);
|
||||
|
|
Loading…
Reference in New Issue