feat(compiler-vapor): invalid html nesting

This commit is contained in:
三咲智子 Kevin Deng 2024-04-18 02:42:59 +08:00
parent a68445bdac
commit 761f785b30
No known key found for this signature in database
8 changed files with 88 additions and 33 deletions

View File

@ -10,9 +10,11 @@ export function render(_ctx) {
`;
exports[`compiler: element transform > component > generate multi root component 1`] = `
"import { createComponent as _createComponent } from 'vue/vapor';
"import { createComponent as _createComponent, template as _template } from 'vue/vapor';
const t0 = _template("123")
export function render(_ctx) {
const n1 = t0()
const n0 = _createComponent(_ctx.Comp)
return [n0, n1]
}"
@ -168,6 +170,23 @@ export function render(_ctx) {
}"
`;
exports[`compiler: element transform > invalid html nesting 1`] = `
"import { insert as _insert, template as _template } from 'vue/vapor';
const t0 = _template("<div>123</div>")
const t1 = _template("<p></p>")
const t2 = _template("<form></form>")
export function render(_ctx) {
const n1 = t1()
const n0 = t0()
const n3 = t2()
const n2 = t2()
_insert(n0, n1)
_insert(n2, n3)
return [n1, n3]
}"
`;
exports[`compiler: element transform > props + children 1`] = `
"import { template as _template } from 'vue/vapor';
const t0 = _template("<div id=\\"foo\\"><span></span></div>")

View File

@ -3,6 +3,7 @@ import {
IRNodeTypes,
transformChildren,
transformElement,
transformText,
transformVBind,
transformVOn,
} from '../../src'
@ -13,7 +14,7 @@ import {
} from '@vue/compiler-core'
const compileWithElementTransform = makeCompile({
nodeTransforms: [transformElement, transformChildren],
nodeTransforms: [transformElement, transformChildren, transformText],
directiveTransforms: {
bind: transformVBind,
on: transformVOn,
@ -689,4 +690,24 @@ describe('compiler: element transform', () => {
])
expect(code).contains('_setDynamicEvents(n0, _ctx.obj)')
})
test('invalid html nesting', () => {
const { code, ir } = compileWithElementTransform(
`<p><div>123</div></p>
<form><form/></form>`,
)
expect(code).toMatchSnapshot()
expect(ir.template).toEqual(['<div>123</div>', '<p></p>', '<form></form>'])
expect(ir.block.dynamic).toMatchObject({
children: [
{ id: 1, template: 1, children: [{ id: 0, template: 0 }] },
{ id: 3, template: 2, children: [{ id: 2, template: 2 }] },
],
})
expect(ir.block.operation).toMatchObject([
{ type: IRNodeTypes.INSERT_NODE, parent: 1, elements: [0] },
{ type: IRNodeTypes.INSERT_NODE, parent: 3, elements: [2] },
])
})
})

View File

@ -39,6 +39,7 @@
"dependencies": {
"@vue/compiler-dom": "workspace:*",
"@vue/shared": "workspace:*",
"source-map-js": "^1.0.2"
"source-map-js": "^1.0.2",
"validate-html-nesting": "^1.2.2"
}
}

View File

@ -78,7 +78,8 @@ export interface TransformContext<T extends AllNode = AllNode> {
enterBlock(ir: TransformContext['block'], isVFor?: boolean): () => void
reference(): number
increaseId(): number
registerTemplate(): number
pushTemplate(template: string): number
registerTemplate(customTemplate?: string): number
registerEffect(
expressions: SimpleExpressionNode[],
operation: OperationNode[],
@ -133,6 +134,7 @@ function createRootContext(
inVFor && this.inVFor++
return () => {
// exit
this.registerTemplate()
this.block = block
this.template = template
this.dynamic = dynamic
@ -180,20 +182,16 @@ function createRootContext(
template: '',
childrenTemplate: [],
pushTemplate(content) {
const existing = root.template.findIndex(template => template === content)
if (existing !== -1) return existing
root.template.push(content)
return root.template.length - 1
},
registerTemplate() {
if (!this.template) {
return -1
}
const existing = root.template.findIndex(
template => template === this.template,
)
if (existing !== -1) {
return (this.dynamic.template = existing)
}
root.template.push(this.template)
return (this.dynamic.template = root.template.length - 1)
if (!this.template) return -1
const id = this.pushTemplate(this.template)
return (this.dynamic.template = id)
},
registerOperation(...node) {
this.block.operation.push(...node)

View File

@ -1,3 +1,4 @@
import { isValidHTMLNesting } from 'validate-html-nesting'
import {
type AttributeNode,
type ElementNode,
@ -44,9 +45,8 @@ export const transformElement: NodeTransform = (node, context) => {
(node.tagType === ElementTypes.ELEMENT ||
node.tagType === ElementTypes.COMPONENT)
)
) {
)
return
}
const { tag, tagType } = node
const isComponent = tagType === ElementTypes.COMPONENT
@ -59,7 +59,7 @@ export const transformElement: NodeTransform = (node, context) => {
;(isComponent ? transformComponentElement : transformNativeElement)(
tag,
propsResult,
context,
context as TransformContext<ElementNode>,
)
}
}
@ -121,12 +121,14 @@ function resolveSetupReference(name: string, context: TransformContext) {
function transformNativeElement(
tag: string,
propsResult: ReturnType<typeof buildProps>,
context: TransformContext,
context: TransformContext<ElementNode>,
) {
const { scopeId } = context.options
context.template += `<${tag}`
if (scopeId) context.template += ` ${scopeId}`
let template = ''
template += `<${tag}`
if (scopeId) template += ` ${scopeId}`
if (propsResult[0] /* dynamic props */) {
const [, dynamicArgs, expressions] = propsResult
@ -141,8 +143,8 @@ function transformNativeElement(
for (const prop of propsResult[1]) {
const { key, values } = prop
if (key.isStatic && values.length === 1 && values[0].isStatic) {
context.template += ` ${key.content}`
if (values[0].content) context.template += `="${values[0].content}"`
template += ` ${key.content}`
if (values[0].content) template += `="${values[0].content}"`
} else {
context.registerEffect(values, [
{
@ -155,10 +157,22 @@ function transformNativeElement(
}
}
context.template += `>` + context.childrenTemplate.join('')
template += `>` + context.childrenTemplate.join('')
// TODO remove unnecessary close tag, e.g. if it's the last element of the template
if (!isVoidTag(tag)) {
context.template += `</${tag}>`
template += `</${tag}>`
}
if (
context.parent &&
context.parent.node.type === NodeTypes.ELEMENT &&
!isValidHTMLNesting(context.parent.node.tag, tag)
) {
context.reference()
context.dynamic.template = context.pushTemplate(template)
context.dynamic.flags |= DynamicFlag.INSERT | DynamicFlag.NON_TEMPLATE
} else {
context.template += template
}
}

View File

@ -64,7 +64,6 @@ export function processFor(
context.reference()
return () => {
context.registerTemplate()
exitBlock()
context.registerOperation({
type: IRNodeTypes.FOR,

View File

@ -128,9 +128,5 @@ export function createIfBranch(
const exitBlock = context.enterBlock(branch)
context.reference()
const onExit = () => {
context.registerTemplate()
exitBlock()
}
return [branch, onExit]
return [branch, exitBlock]
}

View File

@ -292,6 +292,9 @@ importers:
source-map-js:
specifier: ^1.0.2
version: 1.0.2
validate-html-nesting:
specifier: ^1.2.2
version: 1.2.2
packages/dts-built-test:
dependencies:
@ -5968,6 +5971,10 @@ packages:
/util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
/validate-html-nesting@1.2.2:
resolution: {integrity: sha512-hGdgQozCsQJMyfK5urgFcWEqsSSrK63Awe0t/IMR0bZ0QMtnuaiHzThW81guu3qx9abLi99NEuiaN6P9gVYsNg==}
dev: false
/validate-npm-package-license@3.0.4:
resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==}
dependencies: