From: edison Date: Fri, 16 May 2025 01:05:31 +0000 (+0800) Subject: fix(compiler-dom): improve HTML nesting validation to allow any child element within... X-Git-Tag: v3.5.15~24 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=163b3651d174321911648a164052effa9249a2aa;p=thirdparty%2Fvuejs%2Fcore.git fix(compiler-dom): improve HTML nesting validation to allow any child element within template tag (#13320) close #13318 --- diff --git a/packages/compiler-dom/__tests__/transforms/validateHtmlNesting.spec.ts b/packages/compiler-dom/__tests__/transforms/validateHtmlNesting.spec.ts index ad9f917137..46d69846ba 100644 --- a/packages/compiler-dom/__tests__/transforms/validateHtmlNesting.spec.ts +++ b/packages/compiler-dom/__tests__/transforms/validateHtmlNesting.spec.ts @@ -1,4 +1,5 @@ import { type CompilerError, compile } from '../../src' +import { isValidHTMLNesting } from '../../src/htmlNesting' describe('validate html nesting', () => { it('should warn with p > div', () => { @@ -17,4 +18,185 @@ describe('validate html nesting', () => { }) expect(err).toBeUndefined() }) + + // #13318 + it('should not warn when parent tag is template', () => { + let err: CompilerError | undefined + compile(``, { + onWarn: e => (err = e), + }) + expect(err).toBeUndefined() + }) +}) + +/** + * Copied from https://github.com/MananTank/validate-html-nesting + * with ISC license + */ +describe('isValidHTMLNesting', () => { + test('form', () => { + // invalid + expect(isValidHTMLNesting('form', 'form')).toBe(false) + + // valid + expect(isValidHTMLNesting('form', 'div')).toBe(true) + expect(isValidHTMLNesting('form', 'input')).toBe(true) + expect(isValidHTMLNesting('form', 'select')).toBe(true) + expect(isValidHTMLNesting('form', 'button')).toBe(true) + expect(isValidHTMLNesting('form', 'label')).toBe(true) + expect(isValidHTMLNesting('form', 'h1')).toBe(true) + }) + + test('p', () => { + // invalid + expect(isValidHTMLNesting('p', 'p')).toBe(false) + expect(isValidHTMLNesting('p', 'div')).toBe(false) + expect(isValidHTMLNesting('p', 'hr')).toBe(false) + expect(isValidHTMLNesting('p', 'blockquote')).toBe(false) + expect(isValidHTMLNesting('p', 'pre')).toBe(false) + + // valid + expect(isValidHTMLNesting('p', 'a')).toBe(true) + expect(isValidHTMLNesting('p', 'span')).toBe(true) + expect(isValidHTMLNesting('p', 'abbr')).toBe(true) + expect(isValidHTMLNesting('p', 'button')).toBe(true) + expect(isValidHTMLNesting('p', 'b')).toBe(true) + expect(isValidHTMLNesting('p', 'i')).toBe(true) + expect(isValidHTMLNesting('p', 'input')).toBe(true) + expect(isValidHTMLNesting('p', 'label')).toBe(true) + }) + + test('a', () => { + // invalid + expect(isValidHTMLNesting('a', 'a')).toBe(false) + + // valid + expect(isValidHTMLNesting('a', 'div')).toBe(true) + expect(isValidHTMLNesting('a', 'span')).toBe(true) + }) + + test('button', () => { + // invalid + expect(isValidHTMLNesting('button', 'button')).toBe(false) + + // valid + expect(isValidHTMLNesting('button', 'div')).toBe(true) + expect(isValidHTMLNesting('button', 'span')).toBe(true) + }) + + test('table', () => { + // invalid + expect(isValidHTMLNesting('table', 'tr')).toBe(false) + expect(isValidHTMLNesting('table', 'table')).toBe(false) + expect(isValidHTMLNesting('table', 'td')).toBe(false) + + // valid + expect(isValidHTMLNesting('table', 'thead')).toBe(true) + expect(isValidHTMLNesting('table', 'tbody')).toBe(true) + expect(isValidHTMLNesting('table', 'tfoot')).toBe(true) + expect(isValidHTMLNesting('table', 'caption')).toBe(true) + expect(isValidHTMLNesting('table', 'colgroup')).toBe(true) + }) + + test('td', () => { + // valid + expect(isValidHTMLNesting('td', 'span')).toBe(true) + expect(isValidHTMLNesting('tr', 'td')).toBe(true) + + // invalid + expect(isValidHTMLNesting('td', 'td')).toBe(false) + expect(isValidHTMLNesting('div', 'td')).toBe(false) + }) + + test('tbody', () => { + // invalid + expect(isValidHTMLNesting('tbody', 'td')).toBe(false) + + // valid + expect(isValidHTMLNesting('tbody', 'tr')).toBe(true) + }) + + test('tr', () => { + // invalid + expect(isValidHTMLNesting('tr', 'tr')).toBe(false) + expect(isValidHTMLNesting('table', 'tr')).toBe(false) + + // valid + expect(isValidHTMLNesting('tbody', 'tr')).toBe(true) + expect(isValidHTMLNesting('thead', 'tr')).toBe(true) + expect(isValidHTMLNesting('tfoot', 'tr')).toBe(true) + expect(isValidHTMLNesting('tr', 'td')).toBe(true) + expect(isValidHTMLNesting('tr', 'th')).toBe(true) + }) + + test('li', () => { + // invalid + expect(isValidHTMLNesting('li', 'li')).toBe(false) + // valid + expect(isValidHTMLNesting('li', 'div')).toBe(true) + expect(isValidHTMLNesting('li', 'ul')).toBe(true) + }) + + test('headings', () => { + // invalid + expect(isValidHTMLNesting('h1', 'h1')).toBe(false) + expect(isValidHTMLNesting('h2', 'h1')).toBe(false) + expect(isValidHTMLNesting('h3', 'h1')).toBe(false) + expect(isValidHTMLNesting('h1', 'h6')).toBe(false) + + // valid + expect(isValidHTMLNesting('h1', 'div')).toBe(true) + }) + + describe('SVG', () => { + test('svg', () => { + // invalid non-svg tags as children + expect(isValidHTMLNesting('svg', 'div')).toBe(false) + expect(isValidHTMLNesting('svg', 'img')).toBe(false) + expect(isValidHTMLNesting('svg', 'p')).toBe(false) + expect(isValidHTMLNesting('svg', 'h2')).toBe(false) + expect(isValidHTMLNesting('svg', 'span')).toBe(false) + + // valid non-svg tags as children + expect(isValidHTMLNesting('svg', 'a')).toBe(true) + expect(isValidHTMLNesting('svg', 'textarea')).toBe(true) + expect(isValidHTMLNesting('svg', 'input')).toBe(true) + expect(isValidHTMLNesting('svg', 'select')).toBe(true) + + // valid svg tags as children + expect(isValidHTMLNesting('svg', 'g')).toBe(true) + expect(isValidHTMLNesting('svg', 'ellipse')).toBe(true) + expect(isValidHTMLNesting('svg', 'feOffset')).toBe(true) + }) + + test('foreignObject', () => { + // valid + expect(isValidHTMLNesting('foreignObject', 'g')).toBe(true) + expect(isValidHTMLNesting('foreignObject', 'div')).toBe(true) + expect(isValidHTMLNesting('foreignObject', 'a')).toBe(true) + expect(isValidHTMLNesting('foreignObject', 'textarea')).toBe(true) + }) + + test('g', () => { + // valid + expect(isValidHTMLNesting('g', 'div')).toBe(true) + expect(isValidHTMLNesting('g', 'p')).toBe(true) + expect(isValidHTMLNesting('g', 'a')).toBe(true) + expect(isValidHTMLNesting('g', 'textarea')).toBe(true) + expect(isValidHTMLNesting('g', 'g')).toBe(true) + }) + + test('dl', () => { + // valid + expect(isValidHTMLNesting('dl', 'dt')).toBe(true) + expect(isValidHTMLNesting('dl', 'dd')).toBe(true) + expect(isValidHTMLNesting('dl', 'div')).toBe(true) + expect(isValidHTMLNesting('div', 'dt')).toBe(true) + expect(isValidHTMLNesting('div', 'dd')).toBe(true) + + // invalid + expect(isValidHTMLNesting('span', 'dt')).toBe(false) + expect(isValidHTMLNesting('span', 'dd')).toBe(false) + }) + }) }) diff --git a/packages/compiler-dom/src/htmlNesting.ts b/packages/compiler-dom/src/htmlNesting.ts index cb0a7626d1..5f924880bd 100644 --- a/packages/compiler-dom/src/htmlNesting.ts +++ b/packages/compiler-dom/src/htmlNesting.ts @@ -11,6 +11,11 @@ * returns true if given parent-child nesting is valid HTML */ export function isValidHTMLNesting(parent: string, child: string): boolean { + // if the parent is a template, it can have any child + if (parent === 'template') { + return true + } + // 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)