Skip to content

Commit

Permalink
chore: [#1615] Continues on implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
capricorn86 committed Nov 29, 2024
1 parent 38b62c6 commit 62dc2f2
Show file tree
Hide file tree
Showing 4 changed files with 219 additions and 32 deletions.
6 changes: 2 additions & 4 deletions packages/happy-dom/src/dom-parser/DOMParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,16 +33,14 @@ export default class DOMParser {
switch (mimeType) {
case 'text/html':
return <Document>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 <Document>new XMLParser(this[PropertySymbol.window], {
mode: XMLParserModeEnum.xmlDocument,
evaluateScripts: false
mode: XMLParserModeEnum.xmlDocument
}).parse(string);
default:
throw new window.DOMException(`Unknown mime type "${mimeType}".`);
Expand Down
160 changes: 160 additions & 0 deletions packages/happy-dom/src/html-serializer/HTMLSerializer.ts
Original file line number Diff line number Diff line change
@@ -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 = <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 += `<template shadowrootmode="${element.shadowRoot[PropertySymbol.mode]}"${
element.shadowRoot[PropertySymbol.serializable] ? ' shadowrootserializable=""' : ''
}>`;

for (const node of (<ShadowRoot>element.shadowRoot)[PropertySymbol.nodeArray]) {
innerHTML += this.serializeToString(node);
}

innerHTML += '</template>';
}

const childNodes =
localName === 'template'
? (<DocumentFragment>(<HTMLTemplateElement>root).content)[PropertySymbol.nodeArray]
: (<DocumentFragment>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}</${localName}>`;
case Node.DOCUMENT_FRAGMENT_NODE:
case Node.DOCUMENT_NODE:
let html = '';
for (const node of (<Node>root)[PropertySymbol.nodeArray]) {
html += this.serializeToString(node);
}
return html;
case NodeTypeEnum.commentNode:
return `<!--${root.textContent}-->`;
case NodeTypeEnum.processingInstructionNode:
// TODO: Add support for processing instructions.
return `<!--?${(<ProcessingInstruction>root).target} ${root.textContent}?-->`;
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 = <DocumentType>root;
const identifier = doctype.publicId ? ' PUBLIC' : doctype.systemId ? ' SYSTEM' : '';
const publicId = doctype.publicId ? ` "${doctype.publicId}"` : '';
const systemId = doctype.systemId ? ` "${doctype.systemId}"` : '';
return `<!DOCTYPE ${doctype.name}${identifier}${publicId}${systemId}>`;
}

return '';
}

/**
* Returns attributes as a string.
*
* @param element Element.
* @returns Attributes.
*/
private getAttributes(element: Element): string {
let attributeString = '';

if (
!(<Element>element)[PropertySymbol.attributes].getNamedItem('is') &&
(<Element>element)[PropertySymbol.isValue]
) {
attributeString += ' is="' + (<Element>element)[PropertySymbol.isValue] + '"';
}

const namedItems = (<Element>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;
}
}
55 changes: 28 additions & 27 deletions packages/happy-dom/src/xml-serializer/XMLSerializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]: {
Expand Down Expand Up @@ -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}</${localName}>`;
case Node.DOCUMENT_FRAGMENT_NODE:
Expand Down Expand Up @@ -133,27 +134,27 @@ export default class XMLSerializer {

const namedItems = (<Element>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;
Expand Down
30 changes: 29 additions & 1 deletion packages/happy-dom/test/xml-parser/XMLParser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -552,7 +552,8 @@ describe('XMLParser', () => {
<circle cx="5" cy="5" r="4"></circle>
</svg>
</polygon></path></line></ellipse></svg>
</div>`
</div>
`
);
});

Expand Down Expand Up @@ -1146,5 +1147,32 @@ describe('XMLParser', () => {
</div>`
);
});

it('Handles namespaced XML', () => {
const root = new XMLParser(window, { mode: XMLParserModeEnum.xmlDocument }).parse(
`<?xml version="1.0" encoding="UTF-8"?>
<personxml:person xmlns:personxml="http://www.your.example.com/xml/person" xmlns:cityxml="http://www.my.example.com/xml/cities">
<personxml:name>Rob</personxml:name>
<personxml:age>37</personxml:age>
<cityxml:homecity>
<cityxml:name>London</cityxml:name>
<cityxml:lat>123.000</cityxml:lat>
<cityxml:long>0.00</cityxml:long>
</cityxml:homecity>
</personxml:person>`
);

expect(new XMLSerializer().serializeToString(root)).toBe(
`<personxml:person xmlns:personxml="http://www.your.example.com/xml/person" xmlns:cityxml="http://www.my.example.com/xml/cities">
<personxml:name>Rob</personxml:name>
<personxml:age>37</personxml:age>
<cityxml:homecity>
<cityxml:name>London</cityxml:name>
<cityxml:lat>123.000</cityxml:lat>
<cityxml:long>0.00</cityxml:long>
</cityxml:homecity>
</personxml:person>`
);
});
});
});

0 comments on commit 62dc2f2

Please sign in to comment.