diff --git a/packages/happy-dom/src/config/HTMLElementConfig.ts b/packages/happy-dom/src/config/HTMLElementConfig.ts index 76fb7d69a..41a9f0208 100644 --- a/packages/happy-dom/src/config/HTMLElementConfig.ts +++ b/packages/happy-dom/src/config/HTMLElementConfig.ts @@ -331,7 +331,40 @@ export default < }, p: { className: 'HTMLParagraphElement', - contentModel: HTMLElementConfigContentModelEnum.anyDescendants + contentModel: HTMLElementConfigContentModelEnum.noForbiddenFirstLevelDescendants, + forbiddenDescendants: [ + 'address', + 'article', + 'aside', + 'blockquote', + 'details', + 'dialog', + 'div', + 'dl', + 'fieldset', + 'figcaption', + 'figure', + 'footer', + 'form', + 'h1', + 'h2', + 'h3', + 'h4', + 'h5', + 'h6', + 'header', + 'hgroup', + 'hr', + 'main', + 'menu', + 'nav', + 'ol', + 'p', + 'pre', + 'section', + 'table', + 'ul' + ] }, param: { className: 'HTMLParamElement', diff --git a/packages/happy-dom/test/html-parser/HTMLParser.malformedHTML.test.ts b/packages/happy-dom/test/html-parser/HTMLParser.malformedHTML.test.ts new file mode 100644 index 000000000..63da3eea8 --- /dev/null +++ b/packages/happy-dom/test/html-parser/HTMLParser.malformedHTML.test.ts @@ -0,0 +1,106 @@ +import Window from '../../src/window/Window.js'; +import Document from '../../src/nodes/document/Document.js'; +import HTMLParser from '../../src/html-parser/HTMLParser.js'; +import HTMLSerializer from '../../src/html-serializer/HTMLSerializer.js'; +import { beforeEach, describe, it, expect } from 'vitest'; + +/** + * Test cases for GitHub issue #1949: + * DOMParser does not correctly handle malformed HTML + * + * Per the HTML spec, a
element is implicitly closed when certain + * block-level elements are encountered. + * + * @see https://github.com/capricorn86/happy-dom/issues/1949 + * @see https://html.spec.whatwg.org/C/grouping-content.html + */ +describe('HTMLParser - Malformed HTML handling (Issue #1949)', () => { + let window: Window; + + beforeEach(() => { + window = new Window(); + }); + + describe('Paragraph element implicit closing', () => { + it('Should handle the original issue: stray with nested
', () => { + const doc = new window.DOMParser().parseFromString( + '
testing with
new line
', + 'text/html' + ); + expect(doc.body.innerHTML).toBe('testing with
new line
'); + }); + + it('Should closewhen another
is encountered', () => { + const doc = new window.DOMParser().parseFromString( + '
first
second
third
', + 'text/html' + ); + expect(doc.body.innerHTML).toBe('first
second
third
'); + }); + + it('Should closewhen block-level elements are encountered', () => { + // Test representative block elements: div, heading, list, table, sectioning + const testCases = [ + { input: '
text
text
text
text
text
text
text
| cell |
text
| cell |
text
text
text
text
text
quote', + expected: '
text
quote' + } + ]; + + for (const { input, expected } of testCases) { + const doc = new window.DOMParser().parseFromString(input, 'text/html'); + expect(doc.body.innerHTML).toBe(expected); + } + }); + + it('Should NOT close
when inline elements are encountered', () => { + const testCases = [ + { input: '
textinline
', expected: 'textinline
' }, + { input: 'textlink
', expected: 'textlink
' }, + { input: 'textbold
', expected: 'textbold
' } + ]; + + for (const { input, expected } of testCases) { + const doc = new window.DOMParser().parseFromString(input, 'text/html'); + expect(doc.body.innerHTML).toBe(expected); + } + }); + + it('Should handle nested structures correctly', () => { + const doc = new window.DOMParser().parseFromString( + 'first
second
first
second
first
second
'); + expect(new HTMLSerializer().serializeToString(result)).toBe('first
second
'); + }); + }); +});