mirror of https://github.com/vuejs/core.git
dx(compiler-dom): warn on invalid html nesting (#10734)
This commit is contained in:
parent
5f0c6e4a05
commit
a084df1515
|
@ -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()
|
||||||
|
})
|
||||||
|
})
|
|
@ -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,
|
||||||
|
}
|
|
@ -19,13 +19,14 @@ import { transformShow } from './transforms/vShow'
|
||||||
import { transformTransition } from './transforms/Transition'
|
import { transformTransition } from './transforms/Transition'
|
||||||
import { stringifyStatic } from './transforms/stringifyStatic'
|
import { stringifyStatic } from './transforms/stringifyStatic'
|
||||||
import { ignoreSideEffectTags } from './transforms/ignoreSideEffectTags'
|
import { ignoreSideEffectTags } from './transforms/ignoreSideEffectTags'
|
||||||
|
import { validateHtmlNesting } from './transforms/validateHtmlNesting'
|
||||||
import { extend } from '@vue/shared'
|
import { extend } from '@vue/shared'
|
||||||
|
|
||||||
export { parserOptions }
|
export { parserOptions }
|
||||||
|
|
||||||
export const DOMNodeTransforms: NodeTransform[] = [
|
export const DOMNodeTransforms: NodeTransform[] = [
|
||||||
transformStyle,
|
transformStyle,
|
||||||
...(__DEV__ ? [transformTransition] : []),
|
...(__DEV__ ? [transformTransition, validateHtmlNesting] : []),
|
||||||
]
|
]
|
||||||
|
|
||||||
export const DOMDirectiveTransforms: Record<string, DirectiveTransform> = {
|
export const DOMDirectiveTransforms: Record<string, DirectiveTransform> = {
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue