Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: [#1678] Fixes problem with encoding and decoding attribute values in HTML #1680

Merged
merged 5 commits into from
Jan 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading