Skip to content

Commit

Permalink
fix: [#1678] Fixes problem with encoding and decoding attribute value…
Browse files Browse the repository at this point in the history
…s 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
  • Loading branch information
capricorn86 authored Jan 8, 2025
1 parent 978dbfa commit ecbf335
Show file tree
Hide file tree
Showing 9 changed files with 75 additions and 16 deletions.
2 changes: 1 addition & 1 deletion packages/happy-dom/src/html-parser/HTMLParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
4 changes: 2 additions & 2 deletions packages/happy-dom/src/html-serializer/HTMLSerializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 + '"';
Expand Down
4 changes: 3 additions & 1 deletion packages/happy-dom/src/query-selector/SelectorItem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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) {
Expand Down
31 changes: 29 additions & 2 deletions packages/happy-dom/src/utilities/XMLEncodeUtility.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 '';
}
Expand All @@ -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 '';
}
Expand All @@ -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.
*
Expand Down
2 changes: 1 addition & 1 deletion packages/happy-dom/src/xml-parser/XMLParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(':');
Expand Down
12 changes: 6 additions & 6 deletions packages/happy-dom/src/xml-serializer/XMLSerializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
Expand All @@ -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])}"`;
}
}

Expand All @@ -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]
)}"`;
}
Expand Down
20 changes: 20 additions & 0 deletions packages/happy-dom/test/html-parser/HTMLParser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2122,5 +2122,25 @@ describe('HTMLParser', () => {
'<html><head></head><body>Test</body></html>'
);
});

it('Handles line breaks in attributes for #1678', () => {
const result = new HTMLParser(window).parse(
` <div>
<button class="btn btn-secondary comment_reply" data-id="{{id}}" type="button">{{message_gui_reply}}</button> <button class="btn btn-secondary comment_collapse
visually-hidden" type="button">{{message_gui_replies}}</button>
</div>`
);

expect(new HTMLSerializer().serializeToString(result)).toBe(
` <div>
<button class="btn btn-secondary comment_reply" data-id="{{id}}" type="button">{{message_gui_reply}}</button> <button class="btn btn-secondary comment_collapse
visually-hidden" type="button">{{message_gui_replies}}</button>
</div>`
);

const element = result.querySelector('div > .comment_collapse');

expect(element).toBe(result.children[0].children[1]);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,7 @@ describe('HTMLSerializer', () => {
div.setAttribute('attr3', '');

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

Expand Down
14 changes: 12 additions & 2 deletions packages/happy-dom/test/query-selector/QuerySelector.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(<string>(<unknown>12))).toThrow(
Expand Down Expand Up @@ -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(<string>(<unknown>12))).toThrow(
Expand Down Expand Up @@ -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 = `
<div class="class1
class2"></div>
`;

expect(div.querySelector('.class1.class2')).toBe(div.children[0]);
});
});

describe('matches()', () => {
Expand Down

0 comments on commit ecbf335

Please sign in to comment.