From 8c1e0e6ad0eba7ece827d899311cf7d25f64db1a Mon Sep 17 00:00:00 2001 From: Andrew Scott Date: Thu, 10 Jun 2021 15:57:43 -0700 Subject: [PATCH] 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 `
  • `, 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 --- packages/compiler/src/ml_parser/parser.ts | 8 ++++++-- .../compiler/test/ml_parser/html_parser_spec.ts | 14 ++++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/packages/compiler/src/ml_parser/parser.ts b/packages/compiler/src/ml_parser/parser.ts index 905c25d583..24465f8e97 100644 --- a/packages/compiler/src/ml_parser/parser.ts +++ b/packages/compiler/src/ml_parser/parser.ts @@ -313,6 +313,7 @@ class _TreeBuilder { * opening tag is recovered). */ private _popElement(fullName: string, endSourceSpan: ParseSourceSpan|null): boolean { + let unexpectedCloseTagDetected = false; for (let stackIndex = this._elementStack.length - 1; stackIndex >= 0; stackIndex--) { const el = this._elementStack[stackIndex]; if (el.name == fullName) { @@ -323,11 +324,14 @@ class _TreeBuilder { el.sourceSpan.end = endSourceSpan !== null ? endSourceSpan.end : el.sourceSpan.end; this._elementStack.splice(stackIndex, this._elementStack.length - stackIndex); - return true; + return !unexpectedCloseTagDetected; } 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; diff --git a/packages/compiler/test/ml_parser/html_parser_spec.ts b/packages/compiler/test/ml_parser/html_parser_spec.ts index 48e87d07cb..b971d9187a 100644 --- a/packages/compiler/test/ml_parser/html_parser_spec.ts +++ b/packages/compiler/test/ml_parser/html_parser_spec.ts @@ -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('
    ', '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, '
    ', '
    ', '
    '], + [html.Element, 'span', 1, '', '', null], + ]); + }); + describe('incomplete element tag', () => { it('should parse and report incomplete tags after the tag name', () => { const {errors, rootNodes} = parser.parse('
    ', 'TestComp');