dx(compiler-dom): warn on invalid html nesting (#10734)

This commit is contained in:
Kevin Deng 三咲智子 2024-04-22 15:03:39 +08:00 committed by GitHub
parent 5f0c6e4a05
commit a084df1515
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 244 additions and 1 deletions

View File

@ -0,0 +1,20 @@
import { type CompilerError, compile } from '../../src'
describe('validate html nesting', () => {
it('should warn with p > div', () => {
let err: CompilerError | undefined
compile(`<p><div></div></p>`, {
onWarn: e => (err = e),
})
expect(err).toBeDefined()
expect(err!.message).toMatch(`<div> cannot be child of <p>`)
})
it('should not warn with select > hr', () => {
let err: CompilerError | undefined
compile(`<select><hr></select>`, {
onWarn: e => (err = e),
})
expect(err).toBeUndefined()
})
})

View File

@ -0,0 +1,195 @@
/**
* Copied from https://github.com/MananTank/validate-html-nesting
* with ISC license
*
* To avoid runtime dependency on validate-html-nesting
* This file should not change very often in the original repo
* but we may need to keep it up-to-date from time to time.
*/
/**
* returns true if given parent-child nesting is valid HTML
*/
export function isValidHTMLNesting(parent: string, child: string): boolean {
// if we know the list of children that are the only valid children for the given parent
if (parent in onlyValidChildren) {
return onlyValidChildren[parent].has(child)
}
// if we know the list of parents that are the only valid parents for the given child
if (child in onlyValidParents) {
return onlyValidParents[child].has(parent)
}
// if we know the list of children that are NOT valid for the given parent
if (parent in knownInvalidChildren) {
// check if the child is in the list of invalid children
// if so, return false
if (knownInvalidChildren[parent].has(child)) return false
}
// if we know the list of parents that are NOT valid for the given child
if (child in knownInvalidParents) {
// check if the parent is in the list of invalid parents
// if so, return false
if (knownInvalidParents[child].has(parent)) return false
}
return true
}
const headings = new Set(['h1', 'h2', 'h3', 'h4', 'h5', 'h6'])
const emptySet = new Set([])
/**
* maps element to set of elements that can be it's children, no other */
const onlyValidChildren: Record<string, Set<string>> = {
head: new Set([
'base',
'basefront',
'bgsound',
'link',
'meta',
'title',
'noscript',
'noframes',
'style',
'script',
'template',
]),
optgroup: new Set(['option']),
select: new Set(['optgroup', 'option', 'hr']),
// table
table: new Set(['caption', 'colgroup', 'tbody', 'tfoot', 'thead']),
tr: new Set(['td', 'th']),
colgroup: new Set(['col']),
tbody: new Set(['tr']),
thead: new Set(['tr']),
tfoot: new Set(['tr']),
// these elements can not have any children elements
script: emptySet,
iframe: emptySet,
option: emptySet,
textarea: emptySet,
style: emptySet,
title: emptySet,
}
/** maps elements to set of elements which can be it's parent, no other */
const onlyValidParents: Record<string, Set<string>> = {
// sections
html: emptySet,
body: new Set(['html']),
head: new Set(['html']),
// table
td: new Set(['tr']),
colgroup: new Set(['table']),
caption: new Set(['table']),
tbody: new Set(['table']),
tfoot: new Set(['table']),
col: new Set(['colgroup']),
th: new Set(['tr']),
thead: new Set(['table']),
tr: new Set(['tbody', 'thead', 'tfoot']),
// data list
dd: new Set(['dl', 'div']),
dt: new Set(['dl', 'div']),
// other
figcaption: new Set(['figure']),
// li: new Set(["ul", "ol"]),
summary: new Set(['details']),
area: new Set(['map']),
} as const
/** maps element to set of elements that can not be it's children, others can */
const knownInvalidChildren: Record<string, Set<string>> = {
p: new Set([
'address',
'article',
'aside',
'blockquote',
'center',
'details',
'dialog',
'dir',
'div',
'dl',
'fieldset',
'figure',
'footer',
'form',
'h1',
'h2',
'h3',
'h4',
'h5',
'h6',
'header',
'hgroup',
'hr',
'li',
'main',
'nav',
'menu',
'ol',
'p',
'pre',
'section',
'table',
'ul',
]),
svg: new Set([
'b',
'blockquote',
'br',
'code',
'dd',
'div',
'dl',
'dt',
'em',
'embed',
'h1',
'h2',
'h3',
'h4',
'h5',
'h6',
'hr',
'i',
'img',
'li',
'menu',
'meta',
'ol',
'p',
'pre',
'ruby',
's',
'small',
'span',
'strong',
'sub',
'sup',
'table',
'u',
'ul',
'var',
]),
} as const
/** maps element to set of elements that can not be it's parent, others can */
const knownInvalidParents: Record<string, Set<string>> = {
a: new Set(['a']),
button: new Set(['button']),
dd: new Set(['dd', 'dt']),
dt: new Set(['dd', 'dt']),
form: new Set(['form']),
li: new Set(['li']),
h1: headings,
h2: headings,
h3: headings,
h4: headings,
h5: headings,
h6: headings,
}

View File

@ -19,13 +19,14 @@ import { transformShow } from './transforms/vShow'
import { transformTransition } from './transforms/Transition'
import { stringifyStatic } from './transforms/stringifyStatic'
import { ignoreSideEffectTags } from './transforms/ignoreSideEffectTags'
import { validateHtmlNesting } from './transforms/validateHtmlNesting'
import { extend } from '@vue/shared'
export { parserOptions }
export const DOMNodeTransforms: NodeTransform[] = [
transformStyle,
...(__DEV__ ? [transformTransition] : []),
...(__DEV__ ? [transformTransition, validateHtmlNesting] : []),
]
export const DOMDirectiveTransforms: Record<string, DirectiveTransform> = {

View File

@ -0,0 +1,27 @@
import {
type CompilerError,
ElementTypes,
type NodeTransform,
NodeTypes,
} from '@vue/compiler-core'
import { isValidHTMLNesting } from '../htmlNesting'
export const validateHtmlNesting: NodeTransform = (node, context) => {
if (
node.type === NodeTypes.ELEMENT &&
node.tagType === ElementTypes.ELEMENT &&
context.parent &&
context.parent.type === NodeTypes.ELEMENT &&
context.parent.tagType === ElementTypes.ELEMENT &&
!isValidHTMLNesting(context.parent.tag, node.tag)
) {
const error = new SyntaxError(
`<${node.tag}> cannot be child of <${context.parent.tag}>, ` +
'according to HTML specifications. ' +
'This can cause hydration errors or ' +
'potentially disrupt future functionality.',
) as CompilerError
error.loc = node.loc
context.onWarn(error)
}
}