소스 검색

fix(compiler-vapor): preserve close tags across template abbreviation

daiwei 1 개월 전
부모
커밋
7660797f20

+ 28 - 1
packages/compiler-vapor/__tests__/abbreviation.spec.ts

@@ -109,6 +109,33 @@ test('same-name nested tags', () => {
   )
   )
 })
 })
 
 
+test('same-name descendant before an ancestor close', () => {
+  checkAbbr(
+    '<div><div><section><div>x</div></section></div><p>after</p></div>',
+    '<div><div><section><div>x</div></div><p>after',
+    '<div><div><section><div>x</div></section></div><p>after</p></div>',
+  )
+})
+
+test('same-name boundary does not cross component templates', () => {
+  checkAbbr(
+    '<main><div><Comp><div><section><div>x</div></section></div></Comp></div><p>after</p></main>',
+    ['<div><section><div>x', '<main><div></div><p>after'],
+    [
+      '<div><section><div>x</div></section></div>',
+      '<main><div></div><p>after</p></main>',
+    ],
+  )
+})
+
+test('same-name boundary does not cross invalid nesting templates', () => {
+  checkAbbr(
+    '<main><div><p><div>x</div></p></div><section>after</section></main>',
+    ['<div>x', '<main><div><p></div><section>after'],
+    ['<div>x</div>', '<main><div><p></p></div><section>after</section></main>'],
+  )
+})
+
 test('void tags', () => {
 test('void tags', () => {
   // void tags never need closing tags
   // void tags never need closing tags
   checkAbbr('<div><br></div>', '<div><br>', '<div><br></div>')
   checkAbbr('<div><br></div>', '<div><br>', '<div><br></div>')
@@ -257,7 +284,7 @@ test('inline/block ancestor relationships', () => {
   // Both inner divs need closing because they are inside spans that need closing
   // Both inner divs need closing because they are inside spans that need closing
   checkAbbr(
   checkAbbr(
     '<div><span><div><span><div>text</div></span></div></span><p>after</p></div>',
     '<div><span><div><span><div>text</div></span></div></span><p>after</p></div>',
-    '<div><span><div><span><div>text</div></div></span><p>after',
+    '<div><span><div><span><div>text</div></span></div></span><p>after',
     '<div><span><div><span><div>text</div></span></div></span><p>after</p></div>',
     '<div><span><div><span><div>text</div></span></div></span><p>after</p></div>',
   )
   )
 })
 })

+ 5 - 0
packages/compiler-vapor/src/transform.ts

@@ -114,6 +114,10 @@ export class TransformContext<T extends AllNode = AllNode> {
   // whether there is an inline ancestor that needs closing
   // whether there is an inline ancestor that needs closing
   // (i.e. is an inline tag and not on the rightmost path)
   // (i.e. is an inline tag and not on the rightmost path)
   hasInlineAncestorNeedingClose: boolean = false
   hasInlineAncestorNeedingClose: boolean = false
+  // If an ancestor in the same template must close explicitly, descendants
+  // with matching tags must also close so the browser doesn't consume the
+  // ancestor close tag for the descendant.
+  templateCloseTags: Set<string> | undefined = undefined
 
 
   private globalId = 0
   private globalId = 0
   private nextIdMap: Map<number, number> | null = null
   private nextIdMap: Map<number, number> | null = null
@@ -360,6 +364,7 @@ export class TransformContext<T extends AllNode = AllNode> {
       isLastEffectiveChild,
       isLastEffectiveChild,
       isOnRightmostPath,
       isOnRightmostPath,
       hasInlineAncestorNeedingClose,
       hasInlineAncestorNeedingClose,
+      templateCloseTags: this.templateCloseTags,
     } satisfies Partial<TransformContext<T>>)
     } satisfies Partial<TransformContext<T>>)
   }
   }
 
 

+ 19 - 1
packages/compiler-vapor/src/transforms/transformChildren.ts

@@ -10,7 +10,11 @@ import {
   IRNodeTypes,
   IRNodeTypes,
   isBlockOperation,
   isBlockOperation,
 } from '../ir'
 } from '../ir'
-import { shouldUseCreateElement } from './transformElement'
+import {
+  getChildTemplateCloseTags,
+  isInSameTemplateAsParent,
+  shouldUseCreateElement,
+} from './transformElement'
 
 
 export const transformChildren: NodeTransform = (node, context) => {
 export const transformChildren: NodeTransform = (node, context) => {
   const isFragment =
   const isFragment =
@@ -24,9 +28,23 @@ export const transformChildren: NodeTransform = (node, context) => {
   const useCreateElement =
   const useCreateElement =
     node.type === NodeTypes.ELEMENT &&
     node.type === NodeTypes.ELEMENT &&
     shouldUseCreateElement(node, context as TransformContext<ElementNode>)
     shouldUseCreateElement(node, context as TransformContext<ElementNode>)
+  const childTemplateCloseTags =
+    !isFragment && !useCreateElement
+      ? getChildTemplateCloseTags(context as TransformContext<ElementNode>)
+      : undefined
 
 
   for (const [i, child] of node.children.entries()) {
   for (const [i, child] of node.children.entries()) {
     const childContext = context.create(child, i)
     const childContext = context.create(child, i)
+    childContext.templateCloseTags =
+      childTemplateCloseTags &&
+      child.type === NodeTypes.ELEMENT &&
+      child.tagType === ElementTypes.ELEMENT &&
+      isInSameTemplateAsParent(childContext as TransformContext<ElementNode>)
+        ? childTemplateCloseTags
+        : undefined
+    if (isFragment || useCreateElement) {
+      childContext.hasInlineAncestorNeedingClose = false
+    }
     transformNode(childContext)
     transformNode(childContext)
 
 
     const childDynamic = childContext.dynamic
     const childDynamic = childContext.dynamic

+ 61 - 0
packages/compiler-vapor/src/transforms/transformElement.ts

@@ -160,6 +160,15 @@ function canOmitEndTag(
     return true
     return true
   }
   }
 
 
+  if (
+    context.templateCloseTags &&
+    (context.templateCloseTags.has(node.tag) ||
+      isAlwaysCloseTag(node.tag) ||
+      isFormattingTag(node.tag))
+  ) {
+    return false
+  }
+
   // Elements in the alwaysClose list cannot have their end tags omitted
   // Elements in the alwaysClose list cannot have their end tags omitted
   // unless they are on the rightmost path.
   // unless they are on the rightmost path.
   if (isAlwaysCloseTag(node.tag) && !context.isOnRightmostPath) {
   if (isAlwaysCloseTag(node.tag) && !context.isOnRightmostPath) {
@@ -186,6 +195,58 @@ function canOmitEndTag(
   return context.isLastEffectiveChild
   return context.isLastEffectiveChild
 }
 }
 
 
+export function getChildTemplateCloseTags(
+  context: TransformContext<ElementNode>,
+): Set<string> | undefined {
+  const { node } = context
+  if (
+    node.type !== NodeTypes.ELEMENT ||
+    node.tagType !== ElementTypes.ELEMENT ||
+    shouldUseCreateElement(node, context)
+  ) {
+    return
+  }
+
+  const inherited = isInSameTemplateAsParent(context)
+    ? context.templateCloseTags
+    : undefined
+
+  const omitEndTag =
+    context.root === context.effectiveParent ||
+    canOmitEndTag(node as PlainElementNode, context)
+
+  if (omitEndTag || isVoidTag(node.tag)) {
+    return inherited
+  }
+
+  const tags = new Set(inherited)
+  tags.add(node.tag)
+  return tags
+}
+
+export function isInSameTemplateAsParent(
+  context: TransformContext<ElementNode>,
+): boolean {
+  const { parent, node, block } = context
+  if (!parent || block !== parent.block) {
+    return false
+  }
+  const parentNode = parent.node
+  if (
+    parentNode.type !== NodeTypes.ELEMENT ||
+    parentNode.tagType !== ElementTypes.ELEMENT
+  ) {
+    return false
+  }
+
+  return (
+    !shouldUseCreateElement(
+      parentNode,
+      parent as TransformContext<ElementNode>,
+    ) && isValidHTMLNesting(parentNode.tag, node.tag)
+  )
+}
+
 function isSingleRoot(
 function isSingleRoot(
   context: TransformContext<RootNode | TemplateChildNode>,
   context: TransformContext<RootNode | TemplateChildNode>,
 ): boolean {
 ): boolean {