From ecbf335658497b7bbc40f78422fc4feaa376429f Mon Sep 17 00:00:00 2001 From: David Ortner Date: Thu, 9 Jan 2025 00:34:22 +0100 Subject: [PATCH] fix: [#1678] Fixes problem with encoding and decoding attribute values in HTML (#1680) * chore: [#1661] Adds unit test for testing bubbling of events * fix: [#1661] Fixes problem with encoding and decoding attribute values in HTML * chore: [#1678] Fixes problem with query selector --- .../happy-dom/src/html-parser/HTMLParser.ts | 2 +- .../src/html-serializer/HTMLSerializer.ts | 4 +-- .../src/query-selector/SelectorItem.ts | 4 ++- .../src/utilities/XMLEncodeUtility.ts | 31 +++++++++++++++++-- .../happy-dom/src/xml-parser/XMLParser.ts | 2 +- .../src/xml-serializer/XMLSerializer.ts | 12 +++---- .../test/html-parser/HTMLParser.test.ts | 20 ++++++++++++ .../html-serializer/HTMLSerializer.test.ts | 2 +- .../test/query-selector/QuerySelector.test.ts | 14 +++++++-- 9 files changed, 75 insertions(+), 16 deletions(-) diff --git a/packages/happy-dom/src/html-parser/HTMLParser.ts b/packages/happy-dom/src/html-parser/HTMLParser.ts index 6dc2ed98..c5f2ab46 100755 --- a/packages/happy-dom/src/html-parser/HTMLParser.ts +++ b/packages/happy-dom/src/html-parser/HTMLParser.ts @@ -402,7 +402,7 @@ export default class HTMLParser { const name = attributeMatch[1] || attributeMatch[3] || attributeMatch[6] || attributeMatch[9] || ''; const rawValue = attributeMatch[2] || attributeMatch[4] || attributeMatch[7] || ''; - const value = rawValue ? XMLEncodeUtility.decodeAttributeValue(rawValue) : ''; + const value = rawValue ? XMLEncodeUtility.decodeHTMLAttributeValue(rawValue) : ''; const attributes = this.nextElement[PropertySymbol.attributes]; if (this.nextElement[PropertySymbol.namespaceURI] === NamespaceURI.svg) { diff --git a/packages/happy-dom/src/html-serializer/HTMLSerializer.ts b/packages/happy-dom/src/html-serializer/HTMLSerializer.ts index 44b4f459..dede1814 100644 --- a/packages/happy-dom/src/html-serializer/HTMLSerializer.ts +++ b/packages/happy-dom/src/html-serializer/HTMLSerializer.ts @@ -147,11 +147,11 @@ export default class HTMLSerializer { if (!namedItems.has('is') && element[PropertySymbol.isValue]) { attributeString += - ' is="' + XMLEncodeUtility.encodeAttributeValue(element[PropertySymbol.isValue]) + '"'; + ' is="' + XMLEncodeUtility.encodeHTMLAttributeValue(element[PropertySymbol.isValue]) + '"'; } for (const attributes of namedItems.values()) { - const escapedValue = XMLEncodeUtility.encodeAttributeValue( + const escapedValue = XMLEncodeUtility.encodeHTMLAttributeValue( attributes[0][PropertySymbol.value] ); attributeString += ' ' + attributes[0][PropertySymbol.name] + '="' + escapedValue + '"'; diff --git a/packages/happy-dom/src/query-selector/SelectorItem.ts b/packages/happy-dom/src/query-selector/SelectorItem.ts index d27063ff..0bf5b4c5 100644 --- a/packages/happy-dom/src/query-selector/SelectorItem.ts +++ b/packages/happy-dom/src/query-selector/SelectorItem.ts @@ -7,6 +7,8 @@ import ISelectorAttribute from './ISelectorAttribute.js'; import ISelectorMatch from './ISelectorMatch.js'; import ISelectorPseudo from './ISelectorPseudo.js'; +const SPACE_REGEXP = /\s+/; + /** * Selector item. */ @@ -417,7 +419,7 @@ export default class SelectorItem { return null; } - const classList = element.className.split(' '); + const classList = element.className.split(SPACE_REGEXP); let priorityWeight = 0; for (const className of this.classNames) { diff --git a/packages/happy-dom/src/utilities/XMLEncodeUtility.ts b/packages/happy-dom/src/utilities/XMLEncodeUtility.ts index 67ad9c86..8aef0b06 100644 --- a/packages/happy-dom/src/utilities/XMLEncodeUtility.ts +++ b/packages/happy-dom/src/utilities/XMLEncodeUtility.ts @@ -8,7 +8,7 @@ export default class XMLEncodeUtility { * @param value Value. * @returns Escaped value. */ - public static encodeAttributeValue(value: string | null): string { + public static encodeXMLAttributeValue(value: string | null): string { if (value === null) { return ''; } @@ -28,7 +28,7 @@ export default class XMLEncodeUtility { * @param value Value. * @returns Decoded value. */ - public static decodeAttributeValue(value: string | null): string { + public static decodeXMLAttributeValue(value: string | null): string { if (value === null) { return ''; } @@ -43,6 +43,33 @@ export default class XMLEncodeUtility { .replace(/&/gu, '&'); } + /** + * Encodes attribute value. + * + * @param value Value. + * @returns Escaped value. + */ + public static encodeHTMLAttributeValue(value: string | null): string { + if (value === null) { + return ''; + } + return value.replace(/&/gu, '&').replace(/"/gu, '"'); + } + + /** + * Decodes attribute value. + * + * @param value Value. + * @returns Decoded value. + */ + public static decodeHTMLAttributeValue(value: string | null): string { + if (value === null) { + return ''; + } + + return value.replace(/"/gu, '"').replace(/&/gu, '&'); + } + /** * Encodes text content. * diff --git a/packages/happy-dom/src/xml-parser/XMLParser.ts b/packages/happy-dom/src/xml-parser/XMLParser.ts index ea051a38..39bba8d1 100755 --- a/packages/happy-dom/src/xml-parser/XMLParser.ts +++ b/packages/happy-dom/src/xml-parser/XMLParser.ts @@ -487,7 +487,7 @@ export default class XMLParser { // In XML, new line characters should be replaced with a space. const value = rawValue - ? XMLEncodeUtility.decodeAttributeValue(rawValue.replace(NEW_LINE_REGEXP, ' ')) + ? XMLEncodeUtility.decodeXMLAttributeValue(rawValue.replace(NEW_LINE_REGEXP, ' ')) : ''; const attributes = this.nextElement[PropertySymbol.attributes]; const nameParts = name.split(':'); diff --git a/packages/happy-dom/src/xml-serializer/XMLSerializer.ts b/packages/happy-dom/src/xml-serializer/XMLSerializer.ts index f568a5b9..75346e2e 100644 --- a/packages/happy-dom/src/xml-serializer/XMLSerializer.ts +++ b/packages/happy-dom/src/xml-serializer/XMLSerializer.ts @@ -229,7 +229,7 @@ export default class XMLSerializer { attribute[PropertySymbol.localName] === elementPrefix && element[PropertySymbol.namespaceURI] ) { - namespaceString += ` xmlns:${elementPrefix}="${XMLEncodeUtility.encodeAttributeValue( + namespaceString += ` xmlns:${elementPrefix}="${XMLEncodeUtility.encodeXMLAttributeValue( element[PropertySymbol.namespaceURI] )}"`; handledNamespaces.add(element[PropertySymbol.namespaceURI]); @@ -238,20 +238,20 @@ export default class XMLSerializer { attribute[PropertySymbol.name] === 'xmlns' && element[PropertySymbol.namespaceURI] ) { - namespaceString += ` xmlns="${XMLEncodeUtility.encodeAttributeValue( + namespaceString += ` xmlns="${XMLEncodeUtility.encodeXMLAttributeValue( element[PropertySymbol.namespaceURI] )}"`; handledNamespaces.add(element[PropertySymbol.namespaceURI]); } else { namespaceString += ` ${ attribute[PropertySymbol.name] - }="${XMLEncodeUtility.encodeAttributeValue(attribute[PropertySymbol.value])}"`; + }="${XMLEncodeUtility.encodeXMLAttributeValue(attribute[PropertySymbol.value])}"`; handledNamespaces.add(attribute[PropertySymbol.value]); } } else { attributeString += ` ${ attribute[PropertySymbol.name] - }="${XMLEncodeUtility.encodeAttributeValue(attribute[PropertySymbol.value])}"`; + }="${XMLEncodeUtility.encodeXMLAttributeValue(attribute[PropertySymbol.value])}"`; } } @@ -262,14 +262,14 @@ export default class XMLSerializer { !handledNamespaces.has(element[PropertySymbol.namespaceURI]) ) { if (elementPrefix && !inheritedNamespacePrefixes.has(element[PropertySymbol.namespaceURI])) { - namespaceString += ` xmlns:${elementPrefix}="${XMLEncodeUtility.encodeAttributeValue( + namespaceString += ` xmlns:${elementPrefix}="${XMLEncodeUtility.encodeXMLAttributeValue( element[PropertySymbol.namespaceURI] )}"`; } else if ( !elementPrefix && inheritedDefaultNamespace !== element[PropertySymbol.namespaceURI] ) { - namespaceString += ` xmlns="${XMLEncodeUtility.encodeAttributeValue( + namespaceString += ` xmlns="${XMLEncodeUtility.encodeXMLAttributeValue( element[PropertySymbol.namespaceURI] )}"`; } diff --git a/packages/happy-dom/test/html-parser/HTMLParser.test.ts b/packages/happy-dom/test/html-parser/HTMLParser.test.ts index 3c9806d8..a260b197 100644 --- a/packages/happy-dom/test/html-parser/HTMLParser.test.ts +++ b/packages/happy-dom/test/html-parser/HTMLParser.test.ts @@ -2122,5 +2122,25 @@ describe('HTMLParser', () => { 'Test' ); }); + + it('Handles line breaks in attributes for #1678', () => { + const result = new HTMLParser(window).parse( + `
+ +
` + ); + + expect(new HTMLSerializer().serializeToString(result)).toBe( + `
+ +
` + ); + + const element = result.querySelector('div > .comment_collapse'); + + expect(element).toBe(result.children[0].children[1]); + }); }); }); diff --git a/packages/happy-dom/test/html-serializer/HTMLSerializer.test.ts b/packages/happy-dom/test/html-serializer/HTMLSerializer.test.ts index 4efbe4c5..6ae234a9 100644 --- a/packages/happy-dom/test/html-serializer/HTMLSerializer.test.ts +++ b/packages/happy-dom/test/html-serializer/HTMLSerializer.test.ts @@ -254,7 +254,7 @@ describe('HTMLSerializer', () => { div.setAttribute('attr3', ''); expect(serializer.serializeToString(div)).toBe( - '
' + '
' ); }); diff --git a/packages/happy-dom/test/query-selector/QuerySelector.test.ts b/packages/happy-dom/test/query-selector/QuerySelector.test.ts index a72324b5..7e4f9ea8 100644 --- a/packages/happy-dom/test/query-selector/QuerySelector.test.ts +++ b/packages/happy-dom/test/query-selector/QuerySelector.test.ts @@ -18,7 +18,7 @@ describe('QuerySelector', () => { document = window.document; }); - describe('querySelectorAll', () => { + describe('querySelectorAll()', () => { it('Throws an error for invalid selectors.', () => { const container = document.createElement('div'); expect(() => container.querySelectorAll((12))).toThrow( @@ -1214,7 +1214,7 @@ describe('QuerySelector', () => { }); }); - describe('querySelector', () => { + describe('querySelector()', () => { it('Throws an error for invalid selectors.', () => { const container = document.createElement('div'); expect(() => container.querySelector((12))).toThrow( @@ -1628,6 +1628,16 @@ describe('QuerySelector', () => { expect(document.querySelector(':focus')).toBe(div); expect(document.querySelector(':focus-visible')).toBe(div); }); + + it('Handles class names with line breaks', () => { + const div = document.createElement('div'); + div.innerHTML = ` +
+ `; + + expect(div.querySelector('.class1.class2')).toBe(div.children[0]); + }); }); describe('matches()', () => {