Skip to content

Commit ecbf335

Browse files
authored
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
1 parent 978dbfa commit ecbf335

9 files changed

Lines changed: 75 additions & 16 deletions

File tree

packages/happy-dom/src/html-parser/HTMLParser.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -402,7 +402,7 @@ export default class HTMLParser {
402402
const name =
403403
attributeMatch[1] || attributeMatch[3] || attributeMatch[6] || attributeMatch[9] || '';
404404
const rawValue = attributeMatch[2] || attributeMatch[4] || attributeMatch[7] || '';
405-
const value = rawValue ? XMLEncodeUtility.decodeAttributeValue(rawValue) : '';
405+
const value = rawValue ? XMLEncodeUtility.decodeHTMLAttributeValue(rawValue) : '';
406406
const attributes = this.nextElement[PropertySymbol.attributes];
407407

408408
if (this.nextElement[PropertySymbol.namespaceURI] === NamespaceURI.svg) {

packages/happy-dom/src/html-serializer/HTMLSerializer.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -147,11 +147,11 @@ export default class HTMLSerializer {
147147

148148
if (!namedItems.has('is') && element[PropertySymbol.isValue]) {
149149
attributeString +=
150-
' is="' + XMLEncodeUtility.encodeAttributeValue(element[PropertySymbol.isValue]) + '"';
150+
' is="' + XMLEncodeUtility.encodeHTMLAttributeValue(element[PropertySymbol.isValue]) + '"';
151151
}
152152

153153
for (const attributes of namedItems.values()) {
154-
const escapedValue = XMLEncodeUtility.encodeAttributeValue(
154+
const escapedValue = XMLEncodeUtility.encodeHTMLAttributeValue(
155155
attributes[0][PropertySymbol.value]
156156
);
157157
attributeString += ' ' + attributes[0][PropertySymbol.name] + '="' + escapedValue + '"';

packages/happy-dom/src/query-selector/SelectorItem.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import ISelectorAttribute from './ISelectorAttribute.js';
77
import ISelectorMatch from './ISelectorMatch.js';
88
import ISelectorPseudo from './ISelectorPseudo.js';
99

10+
const SPACE_REGEXP = /\s+/;
11+
1012
/**
1113
* Selector item.
1214
*/
@@ -417,7 +419,7 @@ export default class SelectorItem {
417419
return null;
418420
}
419421

420-
const classList = element.className.split(' ');
422+
const classList = element.className.split(SPACE_REGEXP);
421423
let priorityWeight = 0;
422424

423425
for (const className of this.classNames) {

packages/happy-dom/src/utilities/XMLEncodeUtility.ts

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export default class XMLEncodeUtility {
88
* @param value Value.
99
* @returns Escaped value.
1010
*/
11-
public static encodeAttributeValue(value: string | null): string {
11+
public static encodeXMLAttributeValue(value: string | null): string {
1212
if (value === null) {
1313
return '';
1414
}
@@ -28,7 +28,7 @@ export default class XMLEncodeUtility {
2828
* @param value Value.
2929
* @returns Decoded value.
3030
*/
31-
public static decodeAttributeValue(value: string | null): string {
31+
public static decodeXMLAttributeValue(value: string | null): string {
3232
if (value === null) {
3333
return '';
3434
}
@@ -43,6 +43,33 @@ export default class XMLEncodeUtility {
4343
.replace(/&/gu, '&');
4444
}
4545

46+
/**
47+
* Encodes attribute value.
48+
*
49+
* @param value Value.
50+
* @returns Escaped value.
51+
*/
52+
public static encodeHTMLAttributeValue(value: string | null): string {
53+
if (value === null) {
54+
return '';
55+
}
56+
return value.replace(/&/gu, '&').replace(/"/gu, '"');
57+
}
58+
59+
/**
60+
* Decodes attribute value.
61+
*
62+
* @param value Value.
63+
* @returns Decoded value.
64+
*/
65+
public static decodeHTMLAttributeValue(value: string | null): string {
66+
if (value === null) {
67+
return '';
68+
}
69+
70+
return value.replace(/"/gu, '"').replace(/&/gu, '&');
71+
}
72+
4673
/**
4774
* Encodes text content.
4875
*

packages/happy-dom/src/xml-parser/XMLParser.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -487,7 +487,7 @@ export default class XMLParser {
487487

488488
// In XML, new line characters should be replaced with a space.
489489
const value = rawValue
490-
? XMLEncodeUtility.decodeAttributeValue(rawValue.replace(NEW_LINE_REGEXP, ' '))
490+
? XMLEncodeUtility.decodeXMLAttributeValue(rawValue.replace(NEW_LINE_REGEXP, ' '))
491491
: '';
492492
const attributes = this.nextElement[PropertySymbol.attributes];
493493
const nameParts = name.split(':');

packages/happy-dom/src/xml-serializer/XMLSerializer.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,7 @@ export default class XMLSerializer {
229229
attribute[PropertySymbol.localName] === elementPrefix &&
230230
element[PropertySymbol.namespaceURI]
231231
) {
232-
namespaceString += ` xmlns:${elementPrefix}="${XMLEncodeUtility.encodeAttributeValue(
232+
namespaceString += ` xmlns:${elementPrefix}="${XMLEncodeUtility.encodeXMLAttributeValue(
233233
element[PropertySymbol.namespaceURI]
234234
)}"`;
235235
handledNamespaces.add(element[PropertySymbol.namespaceURI]);
@@ -238,20 +238,20 @@ export default class XMLSerializer {
238238
attribute[PropertySymbol.name] === 'xmlns' &&
239239
element[PropertySymbol.namespaceURI]
240240
) {
241-
namespaceString += ` xmlns="${XMLEncodeUtility.encodeAttributeValue(
241+
namespaceString += ` xmlns="${XMLEncodeUtility.encodeXMLAttributeValue(
242242
element[PropertySymbol.namespaceURI]
243243
)}"`;
244244
handledNamespaces.add(element[PropertySymbol.namespaceURI]);
245245
} else {
246246
namespaceString += ` ${
247247
attribute[PropertySymbol.name]
248-
}="${XMLEncodeUtility.encodeAttributeValue(attribute[PropertySymbol.value])}"`;
248+
}="${XMLEncodeUtility.encodeXMLAttributeValue(attribute[PropertySymbol.value])}"`;
249249
handledNamespaces.add(attribute[PropertySymbol.value]);
250250
}
251251
} else {
252252
attributeString += ` ${
253253
attribute[PropertySymbol.name]
254-
}="${XMLEncodeUtility.encodeAttributeValue(attribute[PropertySymbol.value])}"`;
254+
}="${XMLEncodeUtility.encodeXMLAttributeValue(attribute[PropertySymbol.value])}"`;
255255
}
256256
}
257257

@@ -262,14 +262,14 @@ export default class XMLSerializer {
262262
!handledNamespaces.has(element[PropertySymbol.namespaceURI])
263263
) {
264264
if (elementPrefix && !inheritedNamespacePrefixes.has(element[PropertySymbol.namespaceURI])) {
265-
namespaceString += ` xmlns:${elementPrefix}="${XMLEncodeUtility.encodeAttributeValue(
265+
namespaceString += ` xmlns:${elementPrefix}="${XMLEncodeUtility.encodeXMLAttributeValue(
266266
element[PropertySymbol.namespaceURI]
267267
)}"`;
268268
} else if (
269269
!elementPrefix &&
270270
inheritedDefaultNamespace !== element[PropertySymbol.namespaceURI]
271271
) {
272-
namespaceString += ` xmlns="${XMLEncodeUtility.encodeAttributeValue(
272+
namespaceString += ` xmlns="${XMLEncodeUtility.encodeXMLAttributeValue(
273273
element[PropertySymbol.namespaceURI]
274274
)}"`;
275275
}

packages/happy-dom/test/html-parser/HTMLParser.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2122,5 +2122,25 @@ describe('HTMLParser', () => {
21222122
'<html><head></head><body>Test</body></html>'
21232123
);
21242124
});
2125+
2126+
it('Handles line breaks in attributes for #1678', () => {
2127+
const result = new HTMLParser(window).parse(
2128+
` <div>
2129+
<button class="btn btn-secondary comment_reply" data-id="{{id}}" type="button">{{message_gui_reply}}</button> <button class="btn btn-secondary comment_collapse
2130+
visually-hidden" type="button">{{message_gui_replies}}</button>
2131+
</div>`
2132+
);
2133+
2134+
expect(new HTMLSerializer().serializeToString(result)).toBe(
2135+
` <div>
2136+
<button class="btn btn-secondary comment_reply" data-id="{{id}}" type="button">{{message_gui_reply}}</button> <button class="btn btn-secondary comment_collapse
2137+
visually-hidden" type="button">{{message_gui_replies}}</button>
2138+
</div>`
2139+
);
2140+
2141+
const element = result.querySelector('div > .comment_collapse');
2142+
2143+
expect(element).toBe(result.children[0].children[1]);
2144+
});
21252145
});
21262146
});

packages/happy-dom/test/html-serializer/HTMLSerializer.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -254,7 +254,7 @@ describe('HTMLSerializer', () => {
254254
div.setAttribute('attr3', '');
255255

256256
expect(serializer.serializeToString(div)).toBe(
257-
'<div attr1="Hello ⁨John⁩" attr2="&lt;span&gt; test" attr3=""></div>'
257+
'<div attr1="Hello ⁨John⁩" attr2="<span> test" attr3=""></div>'
258258
);
259259
});
260260

packages/happy-dom/test/query-selector/QuerySelector.test.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ describe('QuerySelector', () => {
1818
document = window.document;
1919
});
2020

21-
describe('querySelectorAll', () => {
21+
describe('querySelectorAll()', () => {
2222
it('Throws an error for invalid selectors.', () => {
2323
const container = document.createElement('div');
2424
expect(() => container.querySelectorAll(<string>(<unknown>12))).toThrow(
@@ -1214,7 +1214,7 @@ describe('QuerySelector', () => {
12141214
});
12151215
});
12161216

1217-
describe('querySelector', () => {
1217+
describe('querySelector()', () => {
12181218
it('Throws an error for invalid selectors.', () => {
12191219
const container = document.createElement('div');
12201220
expect(() => container.querySelector(<string>(<unknown>12))).toThrow(
@@ -1628,6 +1628,16 @@ describe('QuerySelector', () => {
16281628
expect(document.querySelector(':focus')).toBe(div);
16291629
expect(document.querySelector(':focus-visible')).toBe(div);
16301630
});
1631+
1632+
it('Handles class names with line breaks', () => {
1633+
const div = document.createElement('div');
1634+
div.innerHTML = `
1635+
<div class="class1
1636+
class2"></div>
1637+
`;
1638+
1639+
expect(div.querySelector('.class1.class2')).toBe(div.children[0]);
1640+
});
16311641
});
16321642

16331643
describe('matches()', () => {

0 commit comments

Comments
 (0)