Skip to content

Commit

Permalink
chore: [#1615] Continues on implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
capricorn86 committed Dec 11, 2024
1 parent c1b1b40 commit dbe90fa
Show file tree
Hide file tree
Showing 11 changed files with 420 additions and 266 deletions.
3 changes: 2 additions & 1 deletion packages/happy-dom/src/exception/DOMExceptionNameEnum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ enum DOMExceptionNameEnum {
abortError = 'AbortError',
timeoutError = 'TimeoutError',
encodingError = 'EncodingError',
uriMismatchError = 'URIMismatchError'
uriMismatchError = 'URIMismatchError',
inUseAttributeError = 'InUseAttributeError'
}
export default DOMExceptionNameEnum;
11 changes: 2 additions & 9 deletions packages/happy-dom/src/html-parser/HTMLParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,17 +200,10 @@ export default class HTMLParser {
} else if (match[2]) {
// End tag.
this.parseEndTag(match[2]);
} else if (
match[3] ||
match[4] ||
(match[6] &&
(<Element>this.currentNode)[PropertySymbol.namespaceURI] === NamespaceURI.html)
) {
} else if (match[3] || match[4]) {
// Comment.
this.parseComment(
match[3] ??
(match[4]?.endsWith('--') ? match[4].slice(0, -2) : match[4] ?? null) ??
match[6]
match[3] ?? (match[4]?.endsWith('--') ? match[4].slice(0, -2) : match[4])
);
} else if (match[5] !== undefined) {
// Document type comment.
Expand Down
2 changes: 1 addition & 1 deletion packages/happy-dom/src/nodes/document/Document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1204,7 +1204,7 @@ export default class Document extends Node {
* @returns Attribute.
*/
public createAttribute(qualifiedName: string): Attr {
return this.createAttributeNS(null, qualifiedName.toLowerCase());
return this.createAttributeNS(null, StringUtility.asciiLowerCase(qualifiedName));
}

/**
Expand Down
6 changes: 3 additions & 3 deletions packages/happy-dom/src/nodes/element/Element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -685,9 +685,9 @@ export default class Element
const namespaceURI = this[PropertySymbol.namespaceURI];
// TODO: Is it correct to check for namespaceURI === NamespaceURI.svg?
const attribute =
namespaceURI === NamespaceURI.svg
? this[PropertySymbol.ownerDocument].createAttributeNS(null, name)
: this[PropertySymbol.ownerDocument].createAttribute(name);
namespaceURI === NamespaceURI.html
? this[PropertySymbol.ownerDocument].createAttribute(name)
: this[PropertySymbol.ownerDocument].createAttributeNS(null, name);
attribute[PropertySymbol.value] = String(value);
this[PropertySymbol.attributes].setNamedItem(attribute);
}
Expand Down
108 changes: 33 additions & 75 deletions packages/happy-dom/src/nodes/element/NamedNodeMap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import DOMException from '../../exception/DOMException.js';
import DOMExceptionNameEnum from '../../exception/DOMExceptionNameEnum.js';
import Element from './Element.js';
import NamespaceURI from '../../config/NamespaceURI.js';
import StringUtility from '../../StringUtility.js';

/**
* Named Node Map.
Expand Down Expand Up @@ -83,14 +84,10 @@ export default class NamedNodeMap {
* @returns Item.
*/
public getNamedItem(name: string): Attr | null {
return (
this[PropertySymbol.namedItems].get(
this[PropertySymbol.getNamedItemKey](
this[PropertySymbol.ownerElement][PropertySymbol.namespaceURI],
name
)
) || null
);
if (this[PropertySymbol.ownerElement][PropertySymbol.namespaceURI] === NamespaceURI.html) {
return this[PropertySymbol.namedItems].get(StringUtility.asciiLowerCase(name)) || null;
}
return this[PropertySymbol.namedItems].get(name) || null;
}

/**
Expand All @@ -101,11 +98,11 @@ export default class NamedNodeMap {
* @returns Item.
*/
public getNamedItemNS(namespace: string, localName: string): Attr | null {
return (
this[PropertySymbol.namespaceItems].get(
this[PropertySymbol.getNamespaceItemKey](namespace, localName)
) || null
);
if (namespace === '') {
namespace = null;
}

return this[PropertySymbol.namespaceItems].get(`${namespace || ''}:${localName}`);
}

/**
Expand Down Expand Up @@ -181,37 +178,33 @@ export default class NamedNodeMap {
* @returns Replaced item.
*/
public [PropertySymbol.setNamedItem](item: Attr, ignoreListeners = false): Attr {
if (!item[PropertySymbol.name]) {
return null;
if (
item[PropertySymbol.ownerElement] !== null &&
item[PropertySymbol.ownerElement] !== this[PropertySymbol.ownerElement]
) {
throw new this[PropertySymbol.ownerElement][PropertySymbol.window].DOMException(
'The attribute is in use.',
DOMExceptionNameEnum.inUseAttributeError
);
}

item[PropertySymbol.ownerElement] = this[PropertySymbol.ownerElement];

const namespaceItemKey = this[PropertySymbol.getNamespaceItemKey](
item[PropertySymbol.namespaceURI],
item[PropertySymbol.name]
);
const replacedItem = this[PropertySymbol.namespaceItems].get(namespaceItemKey) || null;
const replacedNamedItem =
this[PropertySymbol.namedItems].get(item[PropertySymbol.name]) || null;

this[PropertySymbol.namespaceItems].set(namespaceItemKey, item);
const replacedItem =
this.getNamedItemNS(item[PropertySymbol.namespaceURI], item[PropertySymbol.localName]) ||
null;

// The HTML namespace should be prioritized over other namespaces in the namedItems map
// The HTML namespace is the default namespace
if (
(!replacedNamedItem ||
(replacedNamedItem[PropertySymbol.namespaceURI] &&
replacedNamedItem[PropertySymbol.namespaceURI] !== NamespaceURI.html) ||
!item[PropertySymbol.namespaceURI] ||
item[PropertySymbol.namespaceURI] === NamespaceURI.html) &&
// Only lower case names should be stored in the namedItems map
(this[PropertySymbol.ownerElement][PropertySymbol.namespaceURI] !== NamespaceURI.html ||
item[PropertySymbol.name].toLowerCase() === item[PropertySymbol.name])
) {
this[PropertySymbol.namedItems].set(item[PropertySymbol.name], item);
if (replacedItem === item) {
return item;
}

this[PropertySymbol.namespaceItems].set(
`${item[PropertySymbol.namespaceURI] || ''}:${item[PropertySymbol.localName]}`,
item
);

this[PropertySymbol.namedItems].set(item[PropertySymbol.name], item);

if (!ignoreListeners) {
this[PropertySymbol.ownerElement][PropertySymbol.onSetAttribute](item, replacedItem);
}
Expand All @@ -227,49 +220,14 @@ export default class NamedNodeMap {
*/
public [PropertySymbol.removeNamedItem](item: Attr, ignoreListeners = false): void {
item[PropertySymbol.ownerElement] = null;

this[PropertySymbol.namespaceItems].delete(
this[PropertySymbol.getNamespaceItemKey](
item[PropertySymbol.namespaceURI],
item[PropertySymbol.name]
)
);
this[PropertySymbol.namedItems].delete(
this[PropertySymbol.getNamedItemKey](
this[PropertySymbol.ownerElement][PropertySymbol.namespaceURI],
item[PropertySymbol.name]
)
`${item[PropertySymbol.namespaceURI] || ''}:${item[PropertySymbol.localName]}`
);
this[PropertySymbol.namedItems].delete(item[PropertySymbol.name]);

if (!ignoreListeners) {
this[PropertySymbol.ownerElement][PropertySymbol.onRemoveAttribute](item);
}
}

/**
* Returns item name based on namespace.
*
* @param namespaceURI Namespace.
* @param name Name.
* @returns Item name based on namespace.
*/
private [PropertySymbol.getNamedItemKey](namespaceURI: string, name: string): string {
if (!namespaceURI || namespaceURI === NamespaceURI.html) {
return name.toLowerCase();
}
return name;
}

/**
* Returns item key.
*
* @param namespaceURI Namespace.
* @param name Name.
* @returns Key.
*/
private [PropertySymbol.getNamespaceItemKey](namespaceURI: string, name: string): string {
if (!namespaceURI || namespaceURI === NamespaceURI.html) {
return name.toLowerCase();
}
return `${namespaceURI}:${name}`;
}
}
17 changes: 8 additions & 9 deletions packages/happy-dom/src/nodes/element/NamedNodeMapProxyFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,24 +17,23 @@ export default class NamedNodeMapProxyFactory {
*/
public static createProxy(namedNodeMap: NamedNodeMap): NamedNodeMap {
const namedItems = namedNodeMap[PropertySymbol.namedItems];
const namespaceItems = namedNodeMap[PropertySymbol.namespaceItems];

const methodBinder = new ClassMethodBinder(this, [NamedNodeMap]);

return new Proxy<NamedNodeMap>(namedNodeMap, {
get: (target, property) => {
if (property === 'length') {
return namespaceItems.size;
return namedItems.size;
}
if (property in target || typeof property === 'symbol') {
methodBinder.bind(property);
return target[property];
}
const index = Number(property);
if (!isNaN(index)) {
return Array.from(namespaceItems.values())[index];
return Array.from(namedItems.values())[index];
}
return target.getNamedItem(<string>property) || undefined;
return namedItems.get(<string>property) || undefined;
},
set(target, property, newValue): boolean {
methodBinder.bind(property);
Expand All @@ -61,7 +60,7 @@ export default class NamedNodeMapProxyFactory {
},
ownKeys(): string[] {
const keys = Array.from(namedItems.keys());
for (let i = 0, max = namespaceItems.size; i < max; i++) {
for (let i = 0, max = namedItems.size; i < max; i++) {
keys.push(String(i));
}
return keys;
Expand All @@ -77,7 +76,7 @@ export default class NamedNodeMapProxyFactory {

const index = Number(property);

if (!isNaN(index) && index >= 0 && index < namespaceItems.size) {
if (!isNaN(index) && index >= 0 && index < namedItems.size) {
return true;
}

Expand All @@ -100,16 +99,16 @@ export default class NamedNodeMapProxyFactory {

const index = Number(property);

if (!isNaN(index) && index >= 0 && index < namespaceItems.size) {
if (!isNaN(index) && index >= 0 && index < namedItems.size) {
return {
value: Array.from(namespaceItems.values())[index],
value: Array.from(namedItems.values())[index],
writable: false,
enumerable: true,
configurable: true
};
}

const namedItem = target.getNamedItem(<string>property);
const namedItem = namedItems.get(<string>property);

if (namedItem) {
return {
Expand Down
45 changes: 31 additions & 14 deletions packages/happy-dom/src/xml-parser/XMLParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ export default class XMLParser {
} line ${xml.substring(0, this.startTagIndex).split(/\n/g).length} and ${match[2]}\n`;
this.errorIndex = this.markupRegExp.lastIndex;
this.readState = MarkupReadStateEnum.error;
this.removeOverflowingTextNodes();
}
} else if (
match[3] ||
Expand Down Expand Up @@ -196,6 +197,7 @@ export default class XMLParser {
: 'error parsing attribute name\n';
this.errorIndex = match.index;
this.readState = MarkupReadStateEnum.error;
this.removeOverflowingTextNodes();
}
break;
case MarkupReadStateEnum.error:
Expand All @@ -211,17 +213,6 @@ export default class XMLParser {
return this.rootNode;
}

// Missing end tag.
if (this.nodeStack.length !== 1) {
this.parseError(
xml,
this.nextElement
? 'attributes construct error'
: 'Premature end of data in tag article line 1'
);
return this.rootNode;
}

// Missing start tag (e.g. when parsing just a string like "Test").
if (this.rootNode[PropertySymbol.elementArray].length === 0) {
this.parseError('', `Start tag expected, '&lt;' not found`);
Expand All @@ -233,6 +224,17 @@ export default class XMLParser {
this.parsePlainText(xml.substring(this.lastIndex));
}

// Missing end tag.
if (this.nodeStack.length !== 1) {
this.parseError(
xml,
this.nextElement
? 'attributes construct error\n'
: 'Premature end of data in tag article line 1\n'
);
return this.rootNode;
}

return this.rootNode;
}

Expand Down Expand Up @@ -269,7 +271,7 @@ export default class XMLParser {

// When the processing instruction has "xml" as target, we should not add it as a child node.
// Instead we will store the state on the root node, so that it is added when serializing the document with XMLSerializer.
if (parts[0] === 'xml') {
if (parts.length > 1 && parts[0] === 'xml') {
if (
this.currentNode !== this.rootNode ||
this.rootNode[PropertySymbol.elementArray].length !== 0
Expand All @@ -290,9 +292,13 @@ export default class XMLParser {
),
true
);
} else if (parts.length === 1) {
this.errorMessage = 'ParsePI: PI processing-instruction space expected\n';
this.errorIndex = this.markupRegExp.lastIndex - 1;
this.readState = MarkupReadStateEnum.error;
} else {
this.errorMessage = 'error parsing processing instruction\n';
this.errorIndex = this.lastIndex;
this.errorIndex = this.markupRegExp.lastIndex - 1;
this.readState = MarkupReadStateEnum.error;
}
}
Expand Down Expand Up @@ -417,6 +423,7 @@ export default class XMLParser {
: 'attributes construct error\n';
this.errorIndex = this.startTagIndex;
this.readState = MarkupReadStateEnum.error;
this.removeOverflowingTextNodes();
return;
}

Expand Down Expand Up @@ -476,6 +483,9 @@ export default class XMLParser {
);
this.nextElement[PropertySymbol.attributes] = attributes;
attributes[PropertySymbol.ownerElement] = this.nextElement;
for (const attr of attributes[PropertySymbol.namedItems].values()) {
attr[PropertySymbol.ownerElement] = this.nextElement;
}
} else {
this.nextElement[PropertySymbol.namespaceURI] = value;
}
Expand Down Expand Up @@ -511,6 +521,7 @@ export default class XMLParser {
this.errorIndex = this.startTagIndex;
}
this.readState = MarkupReadStateEnum.error;
this.removeOverflowingTextNodes();
return;
}
}
Expand Down Expand Up @@ -612,8 +623,14 @@ export default class XMLParser {
errorElement.innerHTML = `<h3>This page contains the following errors:</h3><div style="font-family:monospace;font-size:12px">${error}</div><h3>Below is a rendering of the page up to the first error.</h3>`;

errorRoot.insertBefore(errorElement, errorRoot.firstChild);
}

// It seems like the browser removes all text nodes in the end of the rendered content.
/**
* Removes overflowing text nodes in the current node.
*
* This needs to be done for some errors.
*/
private removeOverflowingTextNodes(): void {
if (this.currentNode && this.currentNode !== this.rootNode) {
while (this.currentNode.lastChild?.[PropertySymbol.nodeType] === Node.TEXT_NODE) {
this.currentNode.removeChild(this.currentNode.lastChild);
Expand Down
Loading

0 comments on commit dbe90fa

Please sign in to comment.