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: [#1615] Fixes problems related to parsing <html>, <head> and <body> using DOMParser #1617

Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
25053c8
fix: [#1615] Fixes problems related to parsing <html>, <head> and <bo…
capricorn86 Nov 19, 2024
1fb5412
chore: [#1615] Fixes problems related to parsing <html>, <head> and <…
capricorn86 Nov 19, 2024
7e84f3b
chore: [#1615] Continues on implementation
capricorn86 Nov 28, 2024
02c065c
chore: [#1615] Continues on implementation
capricorn86 Nov 29, 2024
38b62c6
chore: [#1615] Continues on implementation
capricorn86 Nov 29, 2024
62dc2f2
chore: [#1615] Continues on implementation
capricorn86 Nov 29, 2024
88cd748
chore: [#1615] Continues on implementation
capricorn86 Nov 29, 2024
3c6ce0e
chore: [#1615] Continues on implementation
capricorn86 Dec 4, 2024
b5d0b2e
chore: [#1615] Continues on implementation
capricorn86 Dec 7, 2024
c1b1b40
chore: [#1615] Continues on implementation
capricorn86 Dec 10, 2024
dbe90fa
chore: [#1615] Continues on implementation
capricorn86 Dec 11, 2024
6782f57
chore: [#1615] Continues on implementation
capricorn86 Dec 13, 2024
8302176
chore: [#1615] Continues on implementation
capricorn86 Dec 13, 2024
2bf2349
chore: [#1615] Continues on implementation
capricorn86 Dec 17, 2024
3ff4650
chore: [#1615] Continues on implementation
capricorn86 Dec 20, 2024
700f16e
chore: [#1615] Continues on implementation
capricorn86 Dec 21, 2024
9407af9
chore: [#1615] Continues on implementation
capricorn86 Dec 21, 2024
e349695
chore: [#1615] Continues on implementation
capricorn86 Dec 26, 2024
e9e2063
chore: [#1615] Continues on implementation
capricorn86 Dec 27, 2024
d981746
chore: [#1615] Continues on implementation
capricorn86 Dec 27, 2024
3dfedb9
Merge branch 'master' into 1615-domparser-does-not-recognise-body-and…
capricorn86 Dec 27, 2024
20fab0a
chore: [#1615] Continues on implementation
capricorn86 Dec 27, 2024
b85782a
chore: [#1615] Continues on implementation
capricorn86 Dec 27, 2024
c55231b
chore: [#1615] Continues on implementation
capricorn86 Dec 27, 2024
a88b5cd
chore: [#1615] Continues on implementation
capricorn86 Dec 27, 2024
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 package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion packages/happy-dom/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,6 @@
"test:debug": "vitest run --inspect-brk --no-file-parallelism"
},
"dependencies": {
"entities": "^4.5.0",
"webidl-conversions": "^7.0.0",
"whatwg-mimetype": "^3.0.0"
},
Expand Down
3 changes: 3 additions & 0 deletions packages/happy-dom/src/PropertySymbol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -375,3 +375,6 @@ export const getLength = Symbol('getLength');
export const currentScale = Symbol('currentScale');
export const rotate = Symbol('rotate');
export const bindMethods = Symbol('bindMethods');
export const hasXMLProcessingInstruction = Symbol('hasXMLProcessingInstruction');
export const root = Symbol('root');
export const filterNode = Symbol('filterNode');
70 changes: 53 additions & 17 deletions packages/happy-dom/src/config/HTMLElementConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,17 @@ import HTMLElementConfigContentModelEnum from './HTMLElementConfigContentModelEn
* @see https://html.spec.whatwg.org/multipage/indices.html
*/
export default <
{ [key: string]: { className: string; contentModel: HTMLElementConfigContentModelEnum } }
{
[key: string]: {
className: string;
contentModel: HTMLElementConfigContentModelEnum;
forbiddenDescendants?: string[];
permittedDescendants?: string[];
permittedParents?: string[];
addPermittedParent?: string;
moveForbiddenDescendant?: { exclude: string[] };
};
}
>{
a: {
className: 'HTMLAnchorElement',
Expand Down Expand Up @@ -116,7 +126,7 @@ export default <
},
caption: {
className: 'HTMLTableCaptionElement',
contentModel: HTMLElementConfigContentModelEnum.anyDescendants
contentModel: HTMLElementConfigContentModelEnum.textOrComments
},
cite: {
className: 'HTMLElement',
Expand All @@ -128,11 +138,13 @@ export default <
},
col: {
className: 'HTMLTableColElement',
contentModel: HTMLElementConfigContentModelEnum.noDescendants
contentModel: HTMLElementConfigContentModelEnum.noDescendants,
permittedParents: ['colgroup']
},
colgroup: {
className: 'HTMLTableColElement',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The class name is the same as the previous?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is correct.

If you run the following code in the browser you will see that both are using the HTMLTableColElement class:

// Outputs "HTMLTableColElement"
console.log(document.createElement('col').constructor.name);

// Outputs "HTMLTableColElement"
console.log(document.createElement('colgroup').constructor.name);

contentModel: HTMLElementConfigContentModelEnum.anyDescendants
contentModel: HTMLElementConfigContentModelEnum.permittedDescendants,
permittedDescendants: ['col']
},
data: {
className: 'HTMLDataElement',
Expand All @@ -144,7 +156,8 @@ export default <
},
dd: {
className: 'HTMLElement',
contentModel: HTMLElementConfigContentModelEnum.noFirstLevelSelfDescendants
contentModel: HTMLElementConfigContentModelEnum.noForbiddenFirstLevelDescendants,
forbiddenDescendants: ['dt', 'dd']
},
del: {
className: 'HTMLModElement',
Expand Down Expand Up @@ -172,7 +185,8 @@ export default <
},
dt: {
className: 'HTMLElement',
contentModel: HTMLElementConfigContentModelEnum.noFirstLevelSelfDescendants
contentModel: HTMLElementConfigContentModelEnum.noForbiddenFirstLevelDescendants,
forbiddenDescendants: ['dt', 'dd']
},
em: {
className: 'HTMLElement',
Expand Down Expand Up @@ -304,11 +318,12 @@ export default <
},
optgroup: {
className: 'HTMLOptGroupElement',
contentModel: HTMLElementConfigContentModelEnum.anyDescendants
contentModel: HTMLElementConfigContentModelEnum.noFirstLevelSelfDescendants
},
option: {
className: 'HTMLOptionElement',
contentModel: HTMLElementConfigContentModelEnum.noFirstLevelSelfDescendants
contentModel: HTMLElementConfigContentModelEnum.noForbiddenFirstLevelDescendants,
forbiddenDescendants: ['option', 'optgroup']
},
output: {
className: 'HTMLOutputElement',
Expand Down Expand Up @@ -344,11 +359,13 @@ export default <
},
rp: {
className: 'HTMLElement',
contentModel: HTMLElementConfigContentModelEnum.anyDescendants
contentModel: HTMLElementConfigContentModelEnum.noForbiddenFirstLevelDescendants,
forbiddenDescendants: ['rp', 'rt']
},
rt: {
className: 'HTMLElement',
contentModel: HTMLElementConfigContentModelEnum.anyDescendants
contentModel: HTMLElementConfigContentModelEnum.noForbiddenFirstLevelDescendants,
forbiddenDescendants: ['rp', 'rt']
},
rtc: {
className: 'HTMLElement',
Expand Down Expand Up @@ -404,27 +421,42 @@ export default <
},
table: {
className: 'HTMLTableElement',
contentModel: HTMLElementConfigContentModelEnum.noFirstLevelSelfDescendants
contentModel: HTMLElementConfigContentModelEnum.permittedDescendants,
permittedDescendants: ['caption', 'colgroup', 'thead', 'tfoot', 'tbody'],
moveForbiddenDescendant: { exclude: [] }
},
tbody: {
className: 'HTMLTableSectionElement',
contentModel: HTMLElementConfigContentModelEnum.anyDescendants
contentModel: HTMLElementConfigContentModelEnum.permittedDescendants,
permittedDescendants: ['tr'],
permittedParents: ['table'],
moveForbiddenDescendant: { exclude: ['caption', 'colgroup', 'thead', 'tfoot', 'tbody'] }
},
td: {
className: 'HTMLTableCellElement',
contentModel: HTMLElementConfigContentModelEnum.anyDescendants
contentModel: HTMLElementConfigContentModelEnum.noForbiddenFirstLevelDescendants,
forbiddenDescendants: ['td', 'th', 'tr', 'tbody', 'tfoot', 'thead'],
permittedParents: ['tr']
},
tfoot: {
className: 'HTMLTableSectionElement',
contentModel: HTMLElementConfigContentModelEnum.anyDescendants
contentModel: HTMLElementConfigContentModelEnum.permittedDescendants,
permittedDescendants: ['tr'],
permittedParents: ['table'],
moveForbiddenDescendant: { exclude: ['caption', 'colgroup', 'thead', 'tfoot', 'tbody'] }
},
th: {
className: 'HTMLTableCellElement',
contentModel: HTMLElementConfigContentModelEnum.anyDescendants
contentModel: HTMLElementConfigContentModelEnum.noForbiddenFirstLevelDescendants,
forbiddenDescendants: ['td', 'th', 'tr', 'tbody', 'tfoot', 'thead'],
permittedParents: ['tr']
},
thead: {
className: 'HTMLTableSectionElement',
contentModel: HTMLElementConfigContentModelEnum.anyDescendants
contentModel: HTMLElementConfigContentModelEnum.permittedDescendants,
permittedDescendants: ['tr'],
permittedParents: ['table'],
moveForbiddenDescendant: { exclude: ['caption', 'colgroup', 'thead', 'tfoot', 'tbody'] }
},
time: {
className: 'HTMLTimeElement',
Expand All @@ -436,7 +468,11 @@ export default <
},
tr: {
className: 'HTMLTableRowElement',
contentModel: HTMLElementConfigContentModelEnum.anyDescendants
contentModel: HTMLElementConfigContentModelEnum.permittedDescendants,
permittedDescendants: ['td', 'th'],
permittedParents: ['tbody', 'tfoot', 'thead'],
addPermittedParent: 'tbody',
moveForbiddenDescendant: { exclude: ['caption', 'colgroup', 'thead', 'tfoot', 'tbody', 'tr'] }
},
track: {
className: 'HTMLTrackElement',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ enum HTMLElementConfigContentModelEnum {
rawText = 'rawText',
noSelfDescendants = 'noSelfDescendants',
noFirstLevelSelfDescendants = 'noFirstLevelSelfDescendants',
noForbiddenFirstLevelDescendants = 'noForbiddenFirstLevelDescendants',
noDescendants = 'noDescendants',
permittedDescendants = 'permittedDescendants',
textOrComments = 'textOrComments',
anyDescendants = 'anyDescendants'
}

Expand Down
2 changes: 2 additions & 0 deletions packages/happy-dom/src/config/NamespaceURI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,7 @@ export default {
html: 'http://www.w3.org/1999/xhtml',
svg: 'http://www.w3.org/2000/svg',
mathML: 'http://www.w3.org/1998/Math/MathML',
xml: 'http://www.w3.org/XML/1998/namespace',
xlink: 'http://www.w3.org/1999/xlink',
xmlns: 'http://www.w3.org/2000/xmlns/'
};
Original file line number Diff line number Diff line change
Expand Up @@ -4963,10 +4963,7 @@ export default class CSSStyleDeclaration {
return new CSSStyleDeclarationComputedStyle(element).getComputedStyle();
}

const attributeValue =
element[PropertySymbol.attributes][PropertySymbol.namedItems].get('style')?.[
PropertySymbol.value
];
const attributeValue = element.getAttribute('style') || '';

if (cache.attributeValue !== attributeValue) {
cache.propertyManager = new CSSStyleDeclarationPropertyManager({ cssText: attributeValue });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -207,12 +207,10 @@ export default class CSSStyleDeclarationComputedStyle {
elementCSSText += cssText.cssText;
}

const elementStyleAttribute = (<Element>parentElement.element)[PropertySymbol.attributes][
PropertySymbol.namedItems
].get('style');
const elementStyleAttribute = (<Element>parentElement.element).getAttribute('style');

if (elementStyleAttribute) {
elementCSSText += elementStyleAttribute[PropertySymbol.value];
elementCSSText += elementStyleAttribute;
}

const rulesAndProperties = CSSStyleDeclarationCSSParser.parse(elementCSSText);
Expand Down
80 changes: 5 additions & 75 deletions packages/happy-dom/src/dom-parser/DOMParser.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import Document from '../nodes/document/Document.js';
import * as PropertySymbol from '../PropertySymbol.js';
import XMLParser from '../xml-parser/XMLParser.js';
import DocumentFragment from '../nodes/document-fragment/DocumentFragment.js';
import BrowserWindow from '../window/BrowserWindow.js';
import NodeTypeEnum from '../nodes/node/NodeTypeEnum.js';
import HTMLParser from '../html-parser/HTMLParser.js';

/**
* DOM parser.
Expand All @@ -29,87 +28,18 @@ export default class DOMParser {
);
}

const newDocument = <Document>this.#createDocument(mimeType);
const documentChildNodes = newDocument[PropertySymbol.nodeArray];

while (documentChildNodes.length) {
newDocument.removeChild(documentChildNodes[0]);
}

const root = <DocumentFragment>XMLParser.parse(newDocument, string, { evaluateScripts: true });
let documentElement = null;
let documentTypeNode = null;

for (const node of root[PropertySymbol.nodeArray]) {
if (node['tagName'] === 'HTML') {
documentElement = node;
} else if (node[PropertySymbol.nodeType] === NodeTypeEnum.documentTypeNode) {
documentTypeNode = node;
}

if (documentElement && documentTypeNode) {
break;
}
}

if (documentElement) {
if (documentTypeNode) {
newDocument.appendChild(documentTypeNode);
}
newDocument.appendChild(documentElement);
const body = newDocument.body;
if (body) {
while (root[PropertySymbol.nodeArray].length) {
body.appendChild(root[PropertySymbol.nodeArray][0]);
}
}
} else {
switch (mimeType) {
case 'image/svg+xml':
{
while (root[PropertySymbol.nodeArray].length) {
newDocument.appendChild(root[PropertySymbol.nodeArray][0]);
}
}
break;
case 'text/html':
default:
{
const documentElement = newDocument.createElement('html');
const bodyElement = newDocument.createElement('body');
const headElement = newDocument.createElement('head');

documentElement.appendChild(headElement);
documentElement.appendChild(bodyElement);
newDocument.appendChild(documentElement);

while (root[PropertySymbol.nodeArray].length) {
bodyElement.appendChild(root[PropertySymbol.nodeArray][0]);
}
}
break;
}
}

return newDocument;
}

/**
*
* @param mimeType Mime type.
* @returns Document.
*/
#createDocument(mimeType: string): Document {
const window = this[PropertySymbol.window];

switch (mimeType) {
case 'text/html':
return new window.HTMLDocument();
const newDocument = new window.HTMLDocument();
newDocument[PropertySymbol.defaultView] = window;
return <Document>new HTMLParser(this[PropertySymbol.window]).parse(string, newDocument);
case 'image/svg+xml':
case 'text/xml':
case 'application/xml':
case 'application/xhtml+xml':
return new window.XMLDocument();
return <Document>new XMLParser(this[PropertySymbol.window]).parse(string);
default:
throw new window.DOMException(`Unknown mime type "${mimeType}".`);
}
Expand Down
22 changes: 12 additions & 10 deletions packages/happy-dom/src/dom/DOMStringMap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,11 @@ export default class DOMStringMap {
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy
return new Proxy(this, {
get(_target, property: string): string {
const attribute = element[PropertySymbol.attributes][PropertySymbol.namedItems].get(
const attribute = element.getAttribute(
'data-' + DOMStringMapUtility.camelCaseToKebab(property)
);
if (attribute) {
return attribute[PropertySymbol.value];
return attribute;
}
},
set(_target, property: string, value: string): boolean {
Expand All @@ -46,19 +46,21 @@ export default class DOMStringMap {
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/Proxy/ownKeys
// "The result List must contain the keys of all non-configurable own properties of the target object."
const keys = [];
for (const item of element[PropertySymbol.attributes][PropertySymbol.namedItems].values()) {
if (item[PropertySymbol.name].startsWith('data-')) {
for (const items of element[PropertySymbol.attributes][
PropertySymbol.namedItems
].values()) {
if (items[0][PropertySymbol.name].startsWith('data-')) {
keys.push(
DOMStringMapUtility.kebabToCamelCase(item[PropertySymbol.name].replace('data-', ''))
DOMStringMapUtility.kebabToCamelCase(
items[0][PropertySymbol.name].replace('data-', '')
)
);
}
}
return keys;
},
has(_target, property: string): boolean {
return element[PropertySymbol.attributes][PropertySymbol.namedItems].has(
'data-' + DOMStringMapUtility.camelCaseToKebab(property)
);
return element.hasAttribute('data-' + DOMStringMapUtility.camelCaseToKebab(property));
},
defineProperty(_target, property: string, descriptor): boolean {
if (descriptor.value === undefined) {
Expand All @@ -73,14 +75,14 @@ export default class DOMStringMap {
return true;
},
getOwnPropertyDescriptor(_target, property: string): PropertyDescriptor {
const attribute = element[PropertySymbol.attributes][PropertySymbol.namedItems].get(
const attribute = element.getAttribute(
'data-' + DOMStringMapUtility.camelCaseToKebab(property)
);
if (!attribute) {
return;
}
return {
value: attribute[PropertySymbol.value],
value: attribute,
writable: true,
enumerable: true,
configurable: true
Expand Down
Loading
Loading