From 89fc131ef87ef0dca4c95960e2b5e3d7cf77070c 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 905c25d583a8c..24465f8e972b0 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 48e87d07cb695..b971d9187a724 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');