From: Kevin Deng 三咲智子 Date: Mon, 22 Apr 2024 07:03:39 +0000 (+0800) Subject: dx(compiler-dom): warn on invalid html nesting (#10734) X-Git-Tag: v3.5.0-alpha.1~9 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=a084df151537276a663b672f3852255a4798a5e0;p=thirdparty%2Fvuejs%2Fcore.git dx(compiler-dom): warn on invalid html nesting (#10734) --- diff --git a/packages/compiler-dom/__tests__/transforms/validateHtmlNesting.spec.ts b/packages/compiler-dom/__tests__/transforms/validateHtmlNesting.spec.ts new file mode 100644 index 0000000000..ad9f917137 --- /dev/null +++ b/packages/compiler-dom/__tests__/transforms/validateHtmlNesting.spec.ts @@ -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(`

`, { + onWarn: e => (err = e), + }) + expect(err).toBeDefined() + expect(err!.message).toMatch(`
cannot be child of

`) + }) + + it('should not warn with select > hr', () => { + let err: CompilerError | undefined + compile(``, { + onWarn: e => (err = e), + }) + expect(err).toBeUndefined() + }) +}) diff --git a/packages/compiler-dom/src/htmlNesting.ts b/packages/compiler-dom/src/htmlNesting.ts new file mode 100644 index 0000000000..cb0a7626d1 --- /dev/null +++ b/packages/compiler-dom/src/htmlNesting.ts @@ -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> = { + 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> = { + // 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> = { + 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> = { + 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, +} diff --git a/packages/compiler-dom/src/index.ts b/packages/compiler-dom/src/index.ts index a3a738a8fb..809f370802 100644 --- a/packages/compiler-dom/src/index.ts +++ b/packages/compiler-dom/src/index.ts @@ -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 = { diff --git a/packages/compiler-dom/src/transforms/validateHtmlNesting.ts b/packages/compiler-dom/src/transforms/validateHtmlNesting.ts new file mode 100644 index 0000000000..540c0c2587 --- /dev/null +++ b/packages/compiler-dom/src/transforms/validateHtmlNesting.ts @@ -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) + } +}