diff --git a/packages/parse5/lib/serializer/index.test.ts b/packages/parse5/lib/serializer/index.test.ts index 5d9dc5516..a51c451c7 100644 --- a/packages/parse5/lib/serializer/index.test.ts +++ b/packages/parse5/lib/serializer/index.test.ts @@ -3,6 +3,7 @@ import * as parse5 from 'parse5'; import { generateSerializerTests } from 'parse5-test-utils/utils/generate-serializer-tests.js'; import { treeAdapters } from 'parse5-test-utils/utils/common.js'; import { type Element, isElementNode } from 'parse5/dist/tree-adapters/default'; +import { NAMESPACES } from 'parse5/dist/common/html.js'; generateSerializerTests('serializer', 'Serializer', parse5.serialize); @@ -42,4 +43,19 @@ describe('serializer', () => { assert.equal(html, ''); }); + + it('serializes the children of void elements as the empty string (GH-289)', () => { + const br = treeAdapters.default.createElement('br', NAMESPACES.HTML, []); + + // Add child node to `br`, to make sure they are skipped. + treeAdapters.default.appendChild(br, treeAdapters.default.createElement('div', NAMESPACES.HTML, [])); + + assert.equal(parse5.serialize(br), ''); + + // If the namespace is not HTML, the serializer should not skip the children. + const svgBr = treeAdapters.default.createElement('br', NAMESPACES.SVG, []); + treeAdapters.default.appendChild(svgBr, treeAdapters.default.createElement('div', NAMESPACES.HTML, [])); + + assert.equal(parse5.serialize(svgBr), '
'); + }); }); diff --git a/packages/parse5/lib/serializer/index.ts b/packages/parse5/lib/serializer/index.ts index d5a9c40d9..aa158d49f 100644 --- a/packages/parse5/lib/serializer/index.ts +++ b/packages/parse5/lib/serializer/index.ts @@ -30,6 +30,15 @@ const VOID_ELEMENTS = new Set([ $.TRACK, $.WBR, ]); + +function isVoidElement(node: T['node'], options: InternalOptions): boolean { + return ( + options.treeAdapter.isElementNode(node) && + options.treeAdapter.getNamespaceURI(node) === NS.HTML && + VOID_ELEMENTS.has(options.treeAdapter.getTagName(node)) + ); +} + const UNESCAPED_TEXT = new Set([$.STYLE, $.SCRIPT, $.XMP, $.IFRAME, $.NOEMBED, $.NOFRAMES, $.PLAINTEXT]); export function hasUnescapedText(tn: string, scriptingEnabled: boolean): boolean { @@ -83,6 +92,11 @@ export function serialize ): string { const opts = { ...defaultOpts, ...options }; + + if (isVoidElement(node, opts)) { + return ''; + } + return serializeChildNodes(node, opts); } @@ -157,9 +171,7 @@ function serializeElement(node: T['element'], opti const tn = options.treeAdapter.getTagName(node); return `<${tn}${serializeAttributes(node, options)}>${ - options.treeAdapter.getNamespaceURI(node) === NS.HTML && VOID_ELEMENTS.has(tn) - ? '' - : `${serializeChildNodes(node, options)}` + isVoidElement(node, options) ? '' : `${serializeChildNodes(node, options)}` }`; }