Просмотр исходного кода

fix(vapor): refined inline-block nesting check for html abbreviation

daiwei 3 месяцев назад
Родитель
Сommit
9220643028

+ 51 - 0
packages/compiler-vapor/__tests__/abbreviation.spec.ts

@@ -180,3 +180,54 @@ test('always close tags', () => {
     '<div><form><input></form></div>',
   )
 })
+
+test('inline/block ancestor relationships', () => {
+  // Inline element containing block element with sibling after inline
+  // The block element must close because inline ancestor needs to close
+  checkAbbr(
+    '<div><span><div>text</div></span><p>after</p></div>',
+    '<div><span><div>text</div></span><p>after',
+    '<div><span><div>text</div></span><p>after</p></div>',
+  )
+
+  // Same situation but deeper nesting
+  checkAbbr(
+    '<div><span><p>text</p></span><span>after</span></div>',
+    '<div><span><p>text</p></span><span>after',
+    '<div><span><p>text</p></span><span>after</span></div>',
+  )
+
+  // Inline containing block on rightmost path - can omit
+  checkAbbr(
+    '<div><span><div>text</div></span></div>',
+    '<div><span><div>text',
+    '<div><span><div>text</div></span></div>',
+  )
+
+  // Normal case - no inline/block issue
+  checkAbbr('<div><p>text</p></div>', '<div><p>text', '<div><p>text</p></div>')
+
+  // Sibling after parent but no inline/block issue
+  checkAbbr(
+    '<div><div><p>text</p></div><span>after</span></div>',
+    '<div><div><p>text</div><span>after',
+    '<div><div><p>text</p></div><span>after</span></div>',
+  )
+
+  // Multi-level inline nesting with block inside
+  // Outer span is not rightmost -> Needs close -> Inner block needs close
+  checkAbbr(
+    '<div><span><b><div>text</div></b></span><p>after</p></div>',
+    '<div><span><b><div>text</div></b></span><p>after',
+    '<div><span><b><div>text</div></b></span><p>after</p></div>',
+  )
+
+  // Mixed nesting: div > span > div > span > div
+  // The middle div is inside a span that needs closing (because of outer structure)
+  // Both inner divs need closing because they are inside spans that need closing
+  checkAbbr(
+    '<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</p></div>',
+  )
+})

+ 31 - 1
packages/compiler-vapor/src/transform.ts

@@ -15,7 +15,14 @@ import {
   getSelfName,
   isVSlot,
 } from '@vue/compiler-dom'
-import { EMPTY_OBJ, NOOP, extend, isArray, isString } from '@vue/shared'
+import {
+  EMPTY_OBJ,
+  NOOP,
+  extend,
+  isArray,
+  isInlineTag,
+  isString,
+} from '@vue/shared'
 import {
   type BlockIRNode,
   DynamicFlag,
@@ -98,6 +105,9 @@ export class TransformContext<T extends AllNode = AllNode> {
   // whether this node is on the rightmost path of the tree
   // (all ancestors are also last effective children)
   isOnRightmostPath: boolean = true
+  // whether there is an inline ancestor that needs closing
+  // (i.e. is an inline tag and not on the rightmost path)
+  hasInlineAncestorNeedingClose: boolean = false
 
   private globalId = 0
   private nextIdMap: Map<number, number> | null = null
@@ -228,6 +238,25 @@ export class TransformContext<T extends AllNode = AllNode> {
     const isLastEffectiveChild = this.isEffectivelyLastChild(index)
     const isOnRightmostPath = this.isOnRightmostPath && isLastEffectiveChild
 
+    // propagate the inline ancestor status
+    let hasInlineAncestorNeedingClose = this.hasInlineAncestorNeedingClose
+    if (this.node.type === NodeTypes.ELEMENT) {
+      if (this.node.tag === 'template') {
+        // <template> acts as a boundary ensuring its content is parsed as a fragment,
+        // protecting inner blocks from outer inline contexts.
+        hasInlineAncestorNeedingClose = false
+      } else if (
+        !hasInlineAncestorNeedingClose &&
+        !this.isOnRightmostPath &&
+        isInlineTag(this.node.tag)
+      ) {
+        // Logic: if current node (parent of the node being created) is inline
+        // AND it's not on the rightmost path, then it needs closing.
+        // Any block child inside will need to be careful.
+        hasInlineAncestorNeedingClose = true
+      }
+    }
+
     return Object.assign(Object.create(TransformContext.prototype), this, {
       node,
       parent: this as any,
@@ -239,6 +268,7 @@ export class TransformContext<T extends AllNode = AllNode> {
       effectiveParent,
       isLastEffectiveChild,
       isOnRightmostPath,
+      hasInlineAncestorNeedingClose,
     } satisfies Partial<TransformContext<T>>)
   }
 

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

@@ -21,6 +21,7 @@ import {
   capitalize,
   extend,
   isAlwaysCloseTag,
+  isBlockTag,
   isBuiltInDirective,
   isFormattingTag,
   isVoidTag,
@@ -156,6 +157,12 @@ function canOmitEndTag(
     return context.isOnRightmostPath
   }
 
+  // For inline element containing block element, if the inline ancestor
+  // is not on rightmost path, the block must close to avoid parsing issues
+  if (isBlockTag(node.tag) && context.hasInlineAncestorNeedingClose) {
+    return false
+  }
+
   return context.isLastEffectiveChild
 }
 

+ 25 - 0
packages/shared/src/domTagConfig.ts

@@ -50,6 +50,19 @@ const ALWAYS_CLOSE_TAGS =
   'title,style,script,noscript,template,' + // raw text / special parsing
   'object,table,button,textarea,select,iframe,fieldset' // scope boundary / form elements
 
+// Inline elements
+const INLINE_TAGS =
+  'a,abbr,acronym,b,bdi,bdo,big,br,button,canvas,cite,code,data,datalist,' +
+  'del,dfn,em,embed,i,iframe,img,input,ins,kbd,label,map,mark,meter,' +
+  'noscript,object,output,picture,progress,q,ruby,s,samp,script,select,' +
+  'small,span,strong,sub,sup,svg,textarea,time,u,tt,var,video'
+
+// Block elements
+const BLOCK_TAGS =
+  'address,article,aside,blockquote,dd,details,dialog,div,dl,dt,fieldset,' +
+  'figcaption,figure,footer,form,h1,h2,h3,h4,h5,h6,header,hgroup,hr,li,' +
+  'main,menu,nav,ol,p,pre,section,table,ul'
+
 /**
  * Compiler only.
  * Do NOT use in runtime code paths unless behind `__DEV__` flag.
@@ -86,3 +99,15 @@ export const isFormattingTag: (key: string) => boolean =
  */
 export const isAlwaysCloseTag: (key: string) => boolean =
   /*@__PURE__*/ makeMap(ALWAYS_CLOSE_TAGS)
+/**
+ * Compiler only.
+ * Do NOT use in runtime code paths unless behind `__DEV__` flag.
+ */
+export const isInlineTag: (key: string) => boolean =
+  /*@__PURE__*/ makeMap(INLINE_TAGS)
+/**
+ * Compiler only.
+ * Do NOT use in runtime code paths unless behind `__DEV__` flag.
+ */
+export const isBlockTag: (key: string) => boolean =
+  /*@__PURE__*/ makeMap(BLOCK_TAGS)