]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
fix(vapor): refined inline-block nesting check for html abbreviation
authordaiwei <daiwei521@126.com>
Tue, 20 Jan 2026 01:58:09 +0000 (09:58 +0800)
committeredison <daiwei521@126.com>
Tue, 20 Jan 2026 02:36:55 +0000 (10:36 +0800)
packages/compiler-vapor/__tests__/abbreviation.spec.ts
packages/compiler-vapor/src/transform.ts
packages/compiler-vapor/src/transforms/transformElement.ts
packages/shared/src/domTagConfig.ts

index c125424861e3b4279e1137cf9d39e0203a697f26..130c6202f407d495738e90d5df693639562c81f0 100644 (file)
@@ -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>',
+  )
+})
index c51e89c34d7502d47a3b96e9f10769fa1f94e26a..357ffaf265f8aca3661d39b35f80a575ecc1da06 100644 (file)
@@ -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>>)
   }
 
index f7e22f2bb95be8301cef6cdaf6c5e11a58ad8557..ffa73a89db6eaa03845014505b9b94a8fc2207c2 100644 (file)
@@ -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
 }
 
index b2a9e78e06e49c14c480cdff35dc0659c3039b41..1d83e6551516f633e427a5d2d8dfbe93fd12dca2 100644 (file)
@@ -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)