From 62dc2f23e81e072765e73f43d26197028c8c51e2 Mon Sep 17 00:00:00 2001 From: David Ortner Date: Fri, 29 Nov 2024 02:00:33 +0100 Subject: [PATCH] chore: [#1615] Continues on implementation --- .../happy-dom/src/dom-parser/DOMParser.ts | 6 +- .../src/html-serializer/HTMLSerializer.ts | 160 ++++++++++++++++++ .../src/xml-serializer/XMLSerializer.ts | 55 +++--- .../test/xml-parser/XMLParser.test.ts | 30 +++- 4 files changed, 219 insertions(+), 32 deletions(-) create mode 100644 packages/happy-dom/src/html-serializer/HTMLSerializer.ts diff --git a/packages/happy-dom/src/dom-parser/DOMParser.ts b/packages/happy-dom/src/dom-parser/DOMParser.ts index d9e5abfb..6494ee79 100644 --- a/packages/happy-dom/src/dom-parser/DOMParser.ts +++ b/packages/happy-dom/src/dom-parser/DOMParser.ts @@ -33,16 +33,14 @@ export default class DOMParser { switch (mimeType) { case 'text/html': return new XMLParser(this[PropertySymbol.window], { - mode: XMLParserModeEnum.htmlDocument, - evaluateScripts: false + mode: XMLParserModeEnum.htmlDocument }).parse(string); case 'image/svg+xml': case 'text/xml': case 'application/xml': case 'application/xhtml+xml': return new XMLParser(this[PropertySymbol.window], { - mode: XMLParserModeEnum.xmlDocument, - evaluateScripts: false + mode: XMLParserModeEnum.xmlDocument }).parse(string); default: throw new window.DOMException(`Unknown mime type "${mimeType}".`); diff --git a/packages/happy-dom/src/html-serializer/HTMLSerializer.ts b/packages/happy-dom/src/html-serializer/HTMLSerializer.ts new file mode 100644 index 00000000..58b90d4d --- /dev/null +++ b/packages/happy-dom/src/html-serializer/HTMLSerializer.ts @@ -0,0 +1,160 @@ +import Element from '../nodes/element/Element.js'; +import * as PropertySymbol from '../PropertySymbol.js'; +import Node from '../nodes/node/Node.js'; +import DocumentType from '../nodes/document-type/DocumentType.js'; +import HTMLTemplateElement from '../nodes/html-template-element/HTMLTemplateElement.js'; +import NodeTypeEnum from '../nodes/node/NodeTypeEnum.js'; +import ProcessingInstruction from '../nodes/processing-instruction/ProcessingInstruction.js'; +import * as Entities from 'entities'; +import DocumentFragment from '../nodes/document-fragment/DocumentFragment.js'; +import ShadowRoot from '../nodes/shadow-root/ShadowRoot.js'; +import HTMLElementConfig from '../config/HTMLElementConfig.js'; +import HTMLElementConfigContentModelEnum from '../config/HTMLElementConfigContentModelEnum.js'; + +/** + * Serializes a node into HTML. + */ +export default class HTMLSerializer { + public [PropertySymbol.options]: { + serializableShadowRoots: boolean; + shadowRoots: ShadowRoot[] | null; + allShadowRoots: boolean; + } = { + serializableShadowRoots: false, + shadowRoots: null, + allShadowRoots: false + }; + + /** + * Renders an element as HTML. + * + * @param root Root element. + * @returns Result. + */ + public serializeToString(root: Node): string { + const options = this[PropertySymbol.options]; + + switch (root[PropertySymbol.nodeType]) { + case NodeTypeEnum.elementNode: + const element = root; + const localName = element[PropertySymbol.localName]; + const config = HTMLElementConfig[element[PropertySymbol.localName]]; + + if (config?.contentModel === HTMLElementConfigContentModelEnum.noDescendants) { + return `<${localName}${this.getAttributes(element)}>`; + } + + let innerHTML = ''; + + // TODO: Should we include closed shadow roots? We are currently only including open shadow roots. + if ( + element.shadowRoot && + (options.allShadowRoots || + (options.serializableShadowRoots && element.shadowRoot[PropertySymbol.serializable]) || + options.shadowRoots?.includes(element.shadowRoot)) + ) { + innerHTML += `'; + } + + const childNodes = + localName === 'template' + ? ((root).content)[PropertySymbol.nodeArray] + : (root)[PropertySymbol.nodeArray]; + + for (const node of childNodes) { + innerHTML += this.serializeToString(node); + } + + // if ( + // !innerHTML && + // (root[PropertySymbol.namespaceURI] === NamespaceURI.xmlns || + // root[PropertySymbol.namespaceURI] === NamespaceURI.svg) + // ) { + // return `<${localName}${this.getAttributes(element)}/>`; + // } + + return `<${localName}${this.getAttributes(element)}>${innerHTML}`; + case Node.DOCUMENT_FRAGMENT_NODE: + case Node.DOCUMENT_NODE: + let html = ''; + for (const node of (root)[PropertySymbol.nodeArray]) { + html += this.serializeToString(node); + } + return html; + case NodeTypeEnum.commentNode: + return ``; + case NodeTypeEnum.processingInstructionNode: + // TODO: Add support for processing instructions. + return ``; + case NodeTypeEnum.textNode: + const parentElement = root.parentElement; + if (parentElement) { + const parentConfig = HTMLElementConfig[parentElement[PropertySymbol.localName]]; + if (parentConfig?.contentModel === HTMLElementConfigContentModelEnum.rawText) { + return root.textContent; + } + } + return Entities.escapeText(root.textContent); + case NodeTypeEnum.documentTypeNode: + const doctype = root; + const identifier = doctype.publicId ? ' PUBLIC' : doctype.systemId ? ' SYSTEM' : ''; + const publicId = doctype.publicId ? ` "${doctype.publicId}"` : ''; + const systemId = doctype.systemId ? ` "${doctype.systemId}"` : ''; + return ``; + } + + return ''; + } + + /** + * Returns attributes as a string. + * + * @param element Element. + * @returns Attributes. + */ + private getAttributes(element: Element): string { + let attributeString = ''; + + if ( + !(element)[PropertySymbol.attributes].getNamedItem('is') && + (element)[PropertySymbol.isValue] + ) { + attributeString += ' is="' + (element)[PropertySymbol.isValue] + '"'; + } + + const namedItems = (element)[PropertySymbol.attributes][PropertySymbol.namedItems]; + + // if ( + // element[PropertySymbol.namespaceURI] === NamespaceURI.svg || + // element[PropertySymbol.namespaceURI] === NamespaceURI.xmlns + // ) { + // const xmlns = namedItems.get('xmlns'); + + // // The "xmlns" attribute should always be the first attribute if it exists. + // if (xmlns && xmlns[PropertySymbol.value]) { + // attributeString += ' xmlns="' + xmlns[PropertySymbol.value] + '"'; + // } + // } + + for (const attribute of namedItems.values()) { + // if ( + // attribute[PropertySymbol.name] !== 'xmlns' || + // (element[PropertySymbol.namespaceURI] !== NamespaceURI.svg && + // element[PropertySymbol.namespaceURI] !== NamespaceURI.xmlns) + // ) { + const escapedValue = Entities.escapeAttribute(attribute[PropertySymbol.value]); + attributeString += ' ' + attribute[PropertySymbol.name] + '="' + escapedValue + '"'; + // } + } + + return attributeString; + } +} diff --git a/packages/happy-dom/src/xml-serializer/XMLSerializer.ts b/packages/happy-dom/src/xml-serializer/XMLSerializer.ts index 7b9b6a62..884b31dd 100644 --- a/packages/happy-dom/src/xml-serializer/XMLSerializer.ts +++ b/packages/happy-dom/src/xml-serializer/XMLSerializer.ts @@ -10,10 +10,11 @@ import DocumentFragment from '../nodes/document-fragment/DocumentFragment.js'; import ShadowRoot from '../nodes/shadow-root/ShadowRoot.js'; import HTMLElementConfig from '../config/HTMLElementConfig.js'; import HTMLElementConfigContentModelEnum from '../config/HTMLElementConfigContentModelEnum.js'; -import NamespaceURI from '../config/NamespaceURI.js'; /** - * Utility for converting an element to string. + * Serializes a node into XML. + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/XMLSerializer */ export default class XMLSerializer { public [PropertySymbol.options]: { @@ -74,13 +75,13 @@ export default class XMLSerializer { innerHTML += this.serializeToString(node); } - if ( - !innerHTML && - (root[PropertySymbol.namespaceURI] === NamespaceURI.xmlns || - root[PropertySymbol.namespaceURI] === NamespaceURI.svg) - ) { - return `<${localName}${this.getAttributes(element)}/>`; - } + // if ( + // !innerHTML && + // (root[PropertySymbol.namespaceURI] === NamespaceURI.xmlns || + // root[PropertySymbol.namespaceURI] === NamespaceURI.svg) + // ) { + // return `<${localName}${this.getAttributes(element)}/>`; + // } return `<${localName}${this.getAttributes(element)}>${innerHTML}`; case Node.DOCUMENT_FRAGMENT_NODE: @@ -133,27 +134,27 @@ export default class XMLSerializer { const namedItems = (element)[PropertySymbol.attributes][PropertySymbol.namedItems]; - if ( - element[PropertySymbol.namespaceURI] === NamespaceURI.svg || - element[PropertySymbol.namespaceURI] === NamespaceURI.xmlns - ) { - const xmlns = namedItems.get('xmlns'); + // if ( + // element[PropertySymbol.namespaceURI] === NamespaceURI.svg || + // element[PropertySymbol.namespaceURI] === NamespaceURI.xmlns + // ) { + // const xmlns = namedItems.get('xmlns'); - // The "xmlns" attribute should always be the first attribute if it exists. - if (xmlns && xmlns[PropertySymbol.value]) { - attributeString += ' xmlns="' + xmlns[PropertySymbol.value] + '"'; - } - } + // // The "xmlns" attribute should always be the first attribute if it exists. + // if (xmlns && xmlns[PropertySymbol.value]) { + // attributeString += ' xmlns="' + xmlns[PropertySymbol.value] + '"'; + // } + // } for (const attribute of namedItems.values()) { - if ( - attribute[PropertySymbol.name] !== 'xmlns' || - (element[PropertySymbol.namespaceURI] !== NamespaceURI.svg && - element[PropertySymbol.namespaceURI] !== NamespaceURI.xmlns) - ) { - const escapedValue = Entities.escapeAttribute(attribute[PropertySymbol.value]); - attributeString += ' ' + attribute[PropertySymbol.name] + '="' + escapedValue + '"'; - } + // if ( + // attribute[PropertySymbol.name] !== 'xmlns' || + // (element[PropertySymbol.namespaceURI] !== NamespaceURI.svg && + // element[PropertySymbol.namespaceURI] !== NamespaceURI.xmlns) + // ) { + const escapedValue = Entities.escapeAttribute(attribute[PropertySymbol.value]); + attributeString += ' ' + attribute[PropertySymbol.name] + '="' + escapedValue + '"'; + // } } return attributeString; diff --git a/packages/happy-dom/test/xml-parser/XMLParser.test.ts b/packages/happy-dom/test/xml-parser/XMLParser.test.ts index 49830fb3..7b3695f2 100644 --- a/packages/happy-dom/test/xml-parser/XMLParser.test.ts +++ b/packages/happy-dom/test/xml-parser/XMLParser.test.ts @@ -552,7 +552,8 @@ describe('XMLParser', () => { - ` + + ` ); }); @@ -1146,5 +1147,32 @@ describe('XMLParser', () => { ` ); }); + + it('Handles namespaced XML', () => { + const root = new XMLParser(window, { mode: XMLParserModeEnum.xmlDocument }).parse( + ` + + Rob + 37 + + London + 123.000 + 0.00 + + ` + ); + + expect(new XMLSerializer().serializeToString(root)).toBe( + ` + Rob + 37 + + London + 123.000 + 0.00 + + ` + ); + }); }); });