'<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>',
+ )
+})
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,
// 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
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,
effectiveParent,
isLastEffectiveChild,
isOnRightmostPath,
+ hasInlineAncestorNeedingClose,
} satisfies Partial<TransformContext<T>>)
}
capitalize,
extend,
isAlwaysCloseTag,
+ isBlockTag,
isBuiltInDirective,
isFormattingTag,
isVoidTag,
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
}
'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.
*/
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)