diff --git a/packages/happy-dom/package.json b/packages/happy-dom/package.json index cae78eddc..40086a71c 100644 --- a/packages/happy-dom/package.json +++ b/packages/happy-dom/package.json @@ -47,7 +47,8 @@ "node-fetch": "^2.x.x", "webidl-conversions": "^7.0.0", "whatwg-encoding": "^2.0.0", - "whatwg-mimetype": "^3.0.0" + "whatwg-mimetype": "^3.0.0", + "iconv-lite": "^0.6.3" }, "devDependencies": { "@types/he": "^1.1.2", diff --git a/packages/happy-dom/src/xml-http-request/XMLHttpRequest.ts b/packages/happy-dom/src/xml-http-request/XMLHttpRequest.ts index 9fc13d972..700ea1354 100644 --- a/packages/happy-dom/src/xml-http-request/XMLHttpRequest.ts +++ b/packages/happy-dom/src/xml-http-request/XMLHttpRequest.ts @@ -17,6 +17,7 @@ import ProgressEvent from '../event/events/ProgressEvent'; import XMLHttpResponseTypeEnum from './XMLHttpResponseTypeEnum'; import XMLHttpRequestCertificate from './XMLHttpRequestCertificate'; import XMLHttpRequestSyncRequestScriptBuilder from './utilities/XMLHttpRequestSyncRequestScriptBuilder'; +import iconv from 'iconv-lite'; // These headers are not user setable. // The following are allowed but banned in the spec: @@ -46,6 +47,8 @@ const FORBIDDEN_REQUEST_HEADERS = [ // These request methods are not allowed const FORBIDDEN_REQUEST_METHODS = ['TRACE', 'TRACK', 'CONNECT']; +// Match Content-Type header charset +const CONTENT_TYPE_ENCODING_REGEXP = /charset=([^;]*)/i; /** * XMLHttpRequest. @@ -595,8 +598,8 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { this._state.status = response.statusCode; this._state.statusText = response.statusMessage; // Sync responseType === '' - this._state.response = response.text; - this._state.responseText = response.text; + this._state.response = this._decodeResponseText(Buffer.from(response.data, 'base64')); + this._state.responseText = this._state.response; this._state.responseXML = null; this._state.responseURL = RelativeURL.getAbsoluteURL( this._ownerDocument.defaultView.location, @@ -755,10 +758,6 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { return; } - if (this._state.incommingMessage && this._state.incommingMessage.setEncoding) { - this._state.incommingMessage.setEncoding('utf-8'); - } - this._setState(XMLHttpRequestReadyStateEnum.headersRecieved); this._state.status = this._state.incommingMessage.statusCode; this._state.statusText = this._state.incommingMessage.statusMessage; @@ -779,7 +778,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { const contentLength = Number(this._state.incommingMessage.headers['content-length']); this.dispatchEvent( new ProgressEvent('progress', { - lengthComputable: isNaN(contentLength) ? false : true, + lengthComputable: !isNaN(contentLength), loaded: tempResponse.length, total: isNaN(contentLength) ? 0 : contentLength }) @@ -954,7 +953,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { case XMLHttpResponseTypeEnum.json: try { return { - response: JSON.parse(data.toString()), + response: JSON.parse(this._decodeResponseText(data)), responseText: null, responseXML: null }; @@ -964,9 +963,10 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { case XMLHttpResponseTypeEnum.text: case '': default: + const responseText = this._decodeResponseText(data); return { - response: data.toString(), - responseText: data.toString(), + response: responseText, + responseText: responseText, responseXML: null }; } @@ -995,4 +995,23 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { this._state.error = true; this._setState(XMLHttpRequestReadyStateEnum.done); } + + /** + * Decodes response text. + * + * @param data Data. + * @returns Decoded text. + **/ + private _decodeResponseText(data: Buffer): string { + const contextTypeEncodingRegexp = new RegExp(CONTENT_TYPE_ENCODING_REGEXP, 'gi'); + let contentType; + if (this._state.incommingMessage && this._state.incommingMessage.headers) { + contentType = this._state.incommingMessage.headers['content-type']; // For remote requests (http/https). + } else { + contentType = this._state.requestHeaders['content-type']; // For local requests or unpredictable remote requests. + } + const charset = contextTypeEncodingRegexp.exec(contentType); + // Default utf-8 + return iconv.decode(data, charset ? charset[1] : 'utf-8'); + } } diff --git a/packages/happy-dom/src/xml-http-request/utilities/XMLHttpRequestSyncRequestScriptBuilder.ts b/packages/happy-dom/src/xml-http-request/utilities/XMLHttpRequestSyncRequestScriptBuilder.ts index 9e8910a1e..00fdb1861 100644 --- a/packages/happy-dom/src/xml-http-request/utilities/XMLHttpRequestSyncRequestScriptBuilder.ts +++ b/packages/happy-dom/src/xml-http-request/utilities/XMLHttpRequestSyncRequestScriptBuilder.ts @@ -23,7 +23,6 @@ export default class XMLHttpRequestSyncRequestScriptBuilder { const request = sendRequest(options, (response) => { let responseText = ''; let responseData = Buffer.alloc(0); - response.setEncoding('utf8'); response.on('data', (chunk) => { responseText += chunk; responseData = Buffer.concat([responseData, Buffer.from(chunk)]); diff --git a/packages/happy-dom/src/xml-parser/XMLParser.ts b/packages/happy-dom/src/xml-parser/XMLParser.ts index fc704af04..b2334a417 100755 --- a/packages/happy-dom/src/xml-parser/XMLParser.ts +++ b/packages/happy-dom/src/xml-parser/XMLParser.ts @@ -13,6 +13,9 @@ import HTMLLinkElement from '../nodes/html-link-element/HTMLLinkElement'; import IDocumentFragment from '../nodes/document-fragment/IDocumentFragment'; import PlainTextElements from '../config/PlainTextElements'; +const CONDITION_COMMENT_REGEXP = + //gi; +const CONDITION_COMMENT_END_REGEXP = //gi; const MARKUP_REGEXP = /<(\/?)([a-z][-.0-9_a-z]*)\s*([^<>]*?)(\/?)>/gi; const COMMENT_REGEXP = /|<([!?])([^>]*)>/gi; const DOCUMENT_TYPE_ATTRIBUTE_REGEXP = /"([^"]+)"/gm; @@ -56,7 +59,23 @@ export default class XMLParser { if (parentTagName && PlainTextElements.includes(parentTagName)) { parent.appendChild(document.createTextNode(text)); } else { - this.appendTextAndCommentNodes(document, parent, text); + let condCommMatch; + let condCommEndMatch; + const condCommRegexp = new RegExp(CONDITION_COMMENT_REGEXP, 'gi'); + const condCommEndRegexp = new RegExp(CONDITION_COMMENT_END_REGEXP, 'gi'); + // @Refer: https://learn.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/?redirectedfrom=MSDN + if ( + isStartTag && + (condCommMatch = condCommRegexp.exec(text)) && + condCommMatch[0] && + (condCommEndMatch = condCommEndRegexp.exec(data.substring(markupRegexp.lastIndex))) && + condCommEndMatch[0] + ) { + markupRegexp.lastIndex += condCommEndRegexp.lastIndex; + continue; + } else { + this.appendTextAndCommentNodes(document, parent, text); + } } } diff --git a/packages/happy-dom/test/xml-parser/XMLParser.test.ts b/packages/happy-dom/test/xml-parser/XMLParser.test.ts index f30c525f4..85dde35a9 100644 --- a/packages/happy-dom/test/xml-parser/XMLParser.test.ts +++ b/packages/happy-dom/test/xml-parser/XMLParser.test.ts @@ -410,5 +410,58 @@ describe('XMLParser', () => { const root4 = XMLParser.parse(window.document, (false)); expect(new XMLSerializer().serializeToString(root4)).toBe('false'); }); + + it('Parses conditional comments', () => { + const testHTML = [ + '', + + '', + + '', + + '', + + '', + + '', + + '\n' + + '\n' + + ' \n' + + ' Title\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + ]; + + for (const html of testHTML) { + const root = XMLParser.parse(window.document, html); + expect(new XMLSerializer().serializeToString(root)).toBe(html); + } + }); }); });