diff --git a/package-lock.json b/package-lock.json index 4fe82f927..4c9ad543e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3520,6 +3520,17 @@ "once": "^1.4.0" } }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -5074,14 +5085,6 @@ "node": ">=0.10.0" } }, - "node_modules/he": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", - "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", - "bin": { - "he": "bin/he" - } - }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -10118,14 +10121,13 @@ "license": "MIT", "dependencies": { "css.escape": "^1.5.1", - "he": "^1.2.0", + "entities": "^4.5.0", "iconv-lite": "^0.6.3", "webidl-conversions": "^7.0.0", "whatwg-encoding": "^2.0.0", "whatwg-mimetype": "^3.0.0" }, "devDependencies": { - "@types/he": "^1.1.2", "@types/jest": "^29.4.0", "@types/node": "^15.6.0", "@types/node-fetch": "^2.6.1", @@ -12960,6 +12962,11 @@ "once": "^1.4.0" } }, + "entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==" + }, "error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -14053,13 +14060,13 @@ "happy-dom": { "version": "file:packages/happy-dom", "requires": { - "@types/he": "^1.1.2", "@types/jest": "^29.4.0", "@types/node": "^15.6.0", "@types/node-fetch": "^2.6.1", "@typescript-eslint/eslint-plugin": "^5.16.0", "@typescript-eslint/parser": "^5.16.0", "css.escape": "^1.5.1", + "entities": "^4.5.0", "eslint": "^8.11.0", "eslint-config-prettier": "^8.5.0", "eslint-plugin-filenames": "^1.3.2", @@ -14069,7 +14076,6 @@ "eslint-plugin-json": "^3.1.0", "eslint-plugin-prettier": "^4.0.0", "eslint-plugin-turbo": "^0.0.7", - "he": "^1.2.0", "iconv-lite": "^0.6.3", "jest": "^29.4.0", "prettier": "^2.6.0", @@ -14196,11 +14202,6 @@ } } }, - "he": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", - "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==" - }, "html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", diff --git a/packages/happy-dom/package.json b/packages/happy-dom/package.json index 26560d5f2..4d419dc1f 100644 --- a/packages/happy-dom/package.json +++ b/packages/happy-dom/package.json @@ -48,14 +48,13 @@ }, "dependencies": { "css.escape": "^1.5.1", - "he": "^1.2.0", + "entities": "^4.5.0", "webidl-conversions": "^7.0.0", "whatwg-encoding": "^2.0.0", "whatwg-mimetype": "^3.0.0", "iconv-lite": "^0.6.3" }, "devDependencies": { - "@types/he": "^1.1.2", "@types/jest": "^29.4.0", "@types/node": "^15.6.0", "@types/node-fetch": "^2.6.1", diff --git a/packages/happy-dom/src/nodes/character-data/CharacterDataUtility.ts b/packages/happy-dom/src/nodes/character-data/CharacterDataUtility.ts index f00799a7d..9023bfaf9 100644 --- a/packages/happy-dom/src/nodes/character-data/CharacterDataUtility.ts +++ b/packages/happy-dom/src/nodes/character-data/CharacterDataUtility.ts @@ -1,26 +1,5 @@ import ICharacterData from './ICharacterData'; -const HTML_ENTITIES = [ - { regex: /"/g, value: '"' }, - { regex: /"/g, value: '"' }, - { regex: /"/g, value: '"' }, - { regex: /&/g, value: '&' }, - { regex: /&/g, value: '&' }, - { regex: /&/g, value: '&' }, - { regex: /'/g, value: "'" }, - { regex: /'/g, value: "'" }, - { regex: /'/g, value: "'" }, - { regex: /</g, value: '<' }, - { regex: /</g, value: '<' }, - { regex: /</g, value: '<' }, - { regex: />/g, value: '>' }, - { regex: />/g, value: '>' }, - { regex: />/g, value: '>' }, - { regex: / /g, value: ' ' }, - { regex: / /g, value: ' ' }, - { regex: / /g, value: ' ' } -]; - /** * Child node utility. */ @@ -90,20 +69,4 @@ export default class CharacterDataUtility { ): string { return characterData.data.substring(offset, offset + count); } - - /** - * Decodes unicode characters to text. - * - * @param html String. - * @returns Decoded HTML string. - */ - public static decodeHTMLEntities(html: string): string { - if (!html) { - return ''; - } - for (const entity of HTML_ENTITIES) { - html = html.replace(entity.regex, entity.value); - } - return html; - } } diff --git a/packages/happy-dom/src/nodes/element/Element.ts b/packages/happy-dom/src/nodes/element/Element.ts index 9bb6083b4..b2421a3aa 100644 --- a/packages/happy-dom/src/nodes/element/Element.ts +++ b/packages/happy-dom/src/nodes/element/Element.ts @@ -30,7 +30,6 @@ import INamedNodeMap from '../../named-node-map/INamedNodeMap'; import Event from '../../event/Event'; import ElementUtility from './ElementUtility'; import HTMLCollection from './HTMLCollection'; -import CharacterDataUtility from '../character-data/CharacterDataUtility'; import EventPhaseEnum from '../../event/EventPhaseEnum'; /** @@ -208,7 +207,7 @@ export default class Element extends Node implements IElement { result += childNode.textContent; } } - return CharacterDataUtility.decodeHTMLEntities(result); + return result; } /** @@ -253,7 +252,7 @@ export default class Element extends Node implements IElement { * @returns HTML. */ public get outerHTML(): string { - return new XMLSerializer().serializeToString(this); + return new XMLSerializer({ escapeEntities: false }).serializeToString(this); } /** @@ -340,10 +339,13 @@ export default class Element extends Node implements IElement { * @returns HTML. */ public getInnerHTML(options?: { includeShadowRoots?: boolean }): string { - const xmlSerializer = new XMLSerializer(); + const xmlSerializer = new XMLSerializer({ + includeShadowRoots: options && options.includeShadowRoots, + escapeEntities: false + }); let xml = ''; for (const node of this.childNodes) { - xml += xmlSerializer.serializeToString(node, options); + xml += xmlSerializer.serializeToString(node); } return xml; } diff --git a/packages/happy-dom/src/nodes/html-template-element/HTMLTemplateElement.ts b/packages/happy-dom/src/nodes/html-template-element/HTMLTemplateElement.ts index 3178c6de5..4cc539a85 100644 --- a/packages/happy-dom/src/nodes/html-template-element/HTMLTemplateElement.ts +++ b/packages/happy-dom/src/nodes/html-template-element/HTMLTemplateElement.ts @@ -50,10 +50,13 @@ export default class HTMLTemplateElement extends HTMLElement implements IHTMLTem * @override */ public getInnerHTML(options?: { includeShadowRoots?: boolean }): string { - const xmlSerializer = new XMLSerializer(); + const xmlSerializer = new XMLSerializer({ + includeShadowRoots: options && options.includeShadowRoots, + escapeEntities: false + }); let xml = ''; for (const node of this.content.childNodes) { - xml += xmlSerializer.serializeToString(node, options); + xml += xmlSerializer.serializeToString(node); } return xml; } diff --git a/packages/happy-dom/src/nodes/shadow-root/ShadowRoot.ts b/packages/happy-dom/src/nodes/shadow-root/ShadowRoot.ts index 3e52c8e14..9e9db64cf 100644 --- a/packages/happy-dom/src/nodes/shadow-root/ShadowRoot.ts +++ b/packages/happy-dom/src/nodes/shadow-root/ShadowRoot.ts @@ -24,7 +24,9 @@ export default class ShadowRoot extends DocumentFragment implements IShadowRoot * @returns HTML. */ public get innerHTML(): string { - const xmlSerializer = new XMLSerializer(); + const xmlSerializer = new XMLSerializer({ + escapeEntities: false + }); let xml = ''; for (const node of this.childNodes) { xml += xmlSerializer.serializeToString(node); diff --git a/packages/happy-dom/src/xml-parser/XMLParser.ts b/packages/happy-dom/src/xml-parser/XMLParser.ts index 0d8e61ef7..7b37084b4 100755 --- a/packages/happy-dom/src/xml-parser/XMLParser.ts +++ b/packages/happy-dom/src/xml-parser/XMLParser.ts @@ -9,7 +9,7 @@ import PlainTextElements from '../config/PlainTextElements'; import IDocumentType from '../nodes/document-type/IDocumentType'; import INode from '../nodes/node/INode'; import IDocumentFragment from '../nodes/document-fragment/IDocumentFragment'; -import { decode } from 'he'; +import * as Entities from 'entities'; /** * Markup RegExp. @@ -99,7 +99,7 @@ export default class XMLParser { // Plain text between tags. currentNode.appendChild( - document.createTextNode(xml.substring(lastIndex, match.index)) + document.createTextNode(Entities.decodeHTML(xml.substring(lastIndex, match.index))) ); } @@ -161,13 +161,17 @@ export default class XMLParser { // Comment. currentNode.appendChild( - document.createComment((match[6] ? '?' : '') + (match[3] || match[4] || match[6])) + document.createComment( + Entities.decodeHTML((match[6] ? '?' : '') + (match[3] || match[4] || match[6])) + ) ); } else if (match[5]) { // Exclamation mark comment (usually ). + const exclamationComment = Entities.decodeHTML(match[5]); currentNode.appendChild( - this.getDocumentTypeNode(document, match[5]) || document.createComment(match[5]) + this.getDocumentTypeNode(document, exclamationComment) || + document.createComment(exclamationComment) ); } else if (match[6]) { // Processing instruction (not supported by HTML). @@ -176,7 +180,9 @@ export default class XMLParser { // Plain text between tags, including the match as it is not a valid start or end tag. currentNode.appendChild( - document.createTextNode(xml.substring(lastIndex, markupRegexp.lastIndex)) + document.createTextNode( + Entities.decodeHTML(xml.substring(lastIndex, markupRegexp.lastIndex)) + ) ); } @@ -205,7 +211,7 @@ export default class XMLParser { const name = attributeMatch[1] || attributeMatch[5] || attributeMatch[9] || ''; const rawValue = attributeMatch[3] || attributeMatch[7] || ''; - const value = rawValue ? decode(rawValue) : ''; + const value = rawValue ? Entities.decodeHTMLAttribute(rawValue) : ''; const namespaceURI = (currentNode).tagName === 'SVG' && name === 'xmlns' ? value : null; @@ -271,7 +277,9 @@ export default class XMLParser { // Plain text elements such as - `.replace(/\s{1,}/, ' ') + `.replace(/\s/g, '') ); }); + + it('Decodes HTML entities.', () => { + const newDocument = domParser.parseFromString( + '

here is some

html elástica ', + 'text/html' + ); + // Spurious comment `` should be solved + expect(newDocument.body.textContent).toBe('here is some html elástica '); + }); }); }); diff --git a/packages/happy-dom/test/nodes/element/Element.test.ts b/packages/happy-dom/test/nodes/element/Element.test.ts index bc55d0831..25183bfa8 100644 --- a/packages/happy-dom/test/nodes/element/Element.test.ts +++ b/packages/happy-dom/test/nodes/element/Element.test.ts @@ -144,7 +144,7 @@ describe('Element', () => { expect(element.textContent).toBe('text1text2'); }); - it('Converts specifial characters to HTML entities.', () => { + it('Returns values HTML entity encoded.', () => { const div = document.createElement('div'); div.innerHTML = '
>
'; expect(div.textContent).toBe('>'); @@ -154,8 +154,8 @@ describe('Element', () => { div.appendChild(el); expect(div.textContent).toBe('>>howdy'); const el2 = document.createElement('div'); - el2.innerHTML = '
><&"'  
'; - expect(el2.textContent).toBe('><&"\' '); + el2.innerHTML = '
><&"' 
'; + expect(el2.textContent).toBe('><&"\'' + String.fromCharCode(160)); const el3 = document.createElement('div'); el3.innerHTML = '<div>Hello, world!</div>'; expect(el3.textContent).toBe('
Hello, world!
'); @@ -393,9 +393,9 @@ describe('Element', () => { jest .spyOn(XMLSerializer.prototype, 'serializeToString') - .mockImplementation((rootElement, options) => { + .mockImplementation(function (rootElement) { expect(rootElement === div).toBe(true); - expect(options).toEqual({ includeShadowRoots: true }); + expect(this._options.includeShadowRoots).toBe(true); return 'EXPECTED_HTML'; }); diff --git a/packages/happy-dom/test/nodes/html-template-element/HTMLTemplateElement.test.ts b/packages/happy-dom/test/nodes/html-template-element/HTMLTemplateElement.test.ts index 61eda7a26..b725d6d7f 100644 --- a/packages/happy-dom/test/nodes/html-template-element/HTMLTemplateElement.test.ts +++ b/packages/happy-dom/test/nodes/html-template-element/HTMLTemplateElement.test.ts @@ -142,9 +142,9 @@ describe('HTMLTemplateElement', () => { jest .spyOn(XMLSerializer.prototype, 'serializeToString') - .mockImplementation((rootElement, options) => { + .mockImplementation(function (rootElement) { expect(rootElement).toBe(div); - expect(options).toEqual({ includeShadowRoots: true }); + expect(this._options.includeShadowRoots).toBe(true); return 'EXPECTED_HTML'; }); diff --git a/packages/happy-dom/test/xml-parser/XMLParser.test.ts b/packages/happy-dom/test/xml-parser/XMLParser.test.ts index f198288fe..9bb6eace9 100644 --- a/packages/happy-dom/test/xml-parser/XMLParser.test.ts +++ b/packages/happy-dom/test/xml-parser/XMLParser.test.ts @@ -251,9 +251,9 @@ describe('XMLParser', () => { expect(new XMLSerializer().serializeToString(root)).toBe( `
- - - + + +
` ); @@ -557,7 +557,7 @@ describe('XMLParser', () => { it('Can read text with ">" in it.', () => { const root = XMLParser.parse(document, `1 > 0`); - expect(new XMLSerializer().serializeToString(root)).toBe(`1 > 0`); + expect(new XMLSerializer().serializeToString(root)).toBe(`1 > 0`); }); it('Parses malformed attributes.', () => { diff --git a/packages/happy-dom/test/xml-serializer/XMLSerializer.test.ts b/packages/happy-dom/test/xml-serializer/XMLSerializer.test.ts index 2f1c263c6..7eaedcd44 100644 --- a/packages/happy-dom/test/xml-serializer/XMLSerializer.test.ts +++ b/packages/happy-dom/test/xml-serializer/XMLSerializer.test.ts @@ -151,7 +151,7 @@ describe('XMLSerializer', () => { document.body.appendChild(div); expect( - xmlSerializer.serializeToString(div, { includeShadowRoots: true }).replace(/[\s]/gm, '') + new XMLSerializer({ includeShadowRoots: true }).serializeToString(div).replace(/[\s]/gm, '') ).toBe( `