fix(compiler): always match close tag to the nearest open element (#42554)

This commit updates the parser logic to continue to try to match an end
tag to an unclosed open tag on the stack. Previously, it would only
push an error to the list and stop looking at unclosed elements.

For example, the invalid HTML of `<li><div></li>`, has an unclosed
element stack of [`li`, `div`] when it encounters the close `li` tag.
We compare against the previously unclosed tag `div` and see that this is
unexpected. Instead of simply giving up here, we continue to move up the
unclosed tags until we find a match (if there is one).

PR Close #42554
This commit is contained in:
Andrew Scott 2021-06-10 15:57:43 -07:00 committed by Alex Rickabaugh
parent 044e0229bd
commit 8c1e0e6ad0
2 changed files with 20 additions and 2 deletions

View File

@ -313,6 +313,7 @@ class _TreeBuilder {
* opening tag is recovered). * opening tag is recovered).
*/ */
private _popElement(fullName: string, endSourceSpan: ParseSourceSpan|null): boolean { private _popElement(fullName: string, endSourceSpan: ParseSourceSpan|null): boolean {
let unexpectedCloseTagDetected = false;
for (let stackIndex = this._elementStack.length - 1; stackIndex >= 0; stackIndex--) { for (let stackIndex = this._elementStack.length - 1; stackIndex >= 0; stackIndex--) {
const el = this._elementStack[stackIndex]; const el = this._elementStack[stackIndex];
if (el.name == fullName) { if (el.name == fullName) {
@ -323,11 +324,14 @@ class _TreeBuilder {
el.sourceSpan.end = endSourceSpan !== null ? endSourceSpan.end : el.sourceSpan.end; el.sourceSpan.end = endSourceSpan !== null ? endSourceSpan.end : el.sourceSpan.end;
this._elementStack.splice(stackIndex, this._elementStack.length - stackIndex); this._elementStack.splice(stackIndex, this._elementStack.length - stackIndex);
return true; return !unexpectedCloseTagDetected;
} }
if (!this.getTagDefinition(el.name).closedByParent) { if (!this.getTagDefinition(el.name).closedByParent) {
return false; // Note that we encountered an unexpected close tag but continue processing the element
// stack so we can assign an `endSourceSpan` if there is a corresponding start tag for this
// end tag in the stack.
unexpectedCloseTagDetected = true;
} }
} }
return false; return false;

View File

@ -857,6 +857,20 @@ import {humanizeDom, humanizeDomSourceSpans, humanizeLineColumn, humanizeNodes}
]]); ]]);
}); });
it('gets correct close tag for parent when a child is not closed', () => {
const {errors, rootNodes} = parser.parse('<div><span></div>', 'TestComp');
expect(errors.length).toEqual(1);
expect(humanizeErrors(errors)).toEqual([[
'div',
'Unexpected closing tag "div". 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',
'0:11'
]]);
expect(humanizeNodes(rootNodes, true)).toEqual([
[html.Element, 'div', 0, '<div><span></div>', '<div>', '</div>'],
[html.Element, 'span', 1, '<span>', '<span>', null],
]);
});
describe('incomplete element tag', () => { describe('incomplete element tag', () => {
it('should parse and report incomplete tags after the tag name', () => { it('should parse and report incomplete tags after the tag name', () => {
const {errors, rootNodes} = parser.parse('<div<span><div </span>', 'TestComp'); const {errors, rootNodes} = parser.parse('<div<span><div </span>', 'TestComp');