Skip to content

Commit c929243

Browse files
authored
fix: [#1728] Fixes incorrect handling of attribute prefixes when iterating NamedNodeMap (#1736)
1 parent 82efdbc commit c929243

File tree

16 files changed

+275
-97
lines changed

16 files changed

+275
-97
lines changed

packages/happy-dom/src/PropertySymbol.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export const isInPassiveEventListener = Symbol('isInPassiveEventListener');
3838
export const isValue = Symbol('isValue');
3939
export const listenerOptions = Symbol('listenerOptions');
4040
export const listeners = Symbol('listeners');
41-
export const namedItems = Symbol('namedItems');
41+
export const itemsByName = Symbol('itemsByName');
4242
export const nextActiveElement = Symbol('nextActiveElement');
4343
export const observeMutations = Symbol('observeMutations');
4444
export const mutationListeners = Symbol('mutationListeners');
@@ -180,7 +180,7 @@ export const getFormControlNamedItem = Symbol('getFormControlNamedItem');
180180
export const dataset = Symbol('dataset');
181181
export const getNamespaceItemKey = Symbol('getNamespaceItemKey');
182182
export const getNamedItemKey = Symbol('getNamedItemKey');
183-
export const namespaceItems = Symbol('namespaceItems');
183+
export const itemsByNamespaceURI = Symbol('itemsByNamespaceURI');
184184
export const proxy = Symbol('proxy');
185185
export const setNamedItem = Symbol('setNamedItem');
186186
export const getTokenList = Symbol('getTokenList');

packages/happy-dom/src/dom/DOMStringMap.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ export default class DOMStringMap {
4747
// "The result List must contain the keys of all non-configurable own properties of the target object."
4848
const keys = [];
4949
for (const items of element[PropertySymbol.attributes][
50-
PropertySymbol.namedItems
50+
PropertySymbol.itemsByName
5151
].values()) {
5252
if (items[0][PropertySymbol.name].startsWith('data-')) {
5353
keys.push(

packages/happy-dom/src/exception/DOMExceptionNameEnum.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ enum DOMExceptionNameEnum {
1717
timeoutError = 'TimeoutError',
1818
encodingError = 'EncodingError',
1919
uriMismatchError = 'URIMismatchError',
20-
inUseAttributeError = 'InUseAttributeError'
20+
inUseAttributeError = 'InUseAttributeError',
21+
namespaceError = 'NamespaceError'
2122
}
2223
export default DOMExceptionNameEnum;

packages/happy-dom/src/html-serializer/HTMLSerializer.ts

+5-5
Original file line numberDiff line numberDiff line change
@@ -143,18 +143,18 @@ export default class HTMLSerializer {
143143
private getAttributes(element: Element): string {
144144
let attributeString = '';
145145

146-
const namedItems = (<Element>element)[PropertySymbol.attributes][PropertySymbol.namedItems];
146+
const attributes = (<Element>element)[PropertySymbol.attributes][PropertySymbol.items];
147147

148-
if (!namedItems.has('is') && element[PropertySymbol.isValue]) {
148+
if (!attributes.has(':is') && element[PropertySymbol.isValue]) {
149149
attributeString +=
150150
' is="' + XMLEncodeUtility.encodeHTMLAttributeValue(element[PropertySymbol.isValue]) + '"';
151151
}
152152

153-
for (const attributes of namedItems.values()) {
153+
for (const attribute of attributes.values()) {
154154
const escapedValue = XMLEncodeUtility.encodeHTMLAttributeValue(
155-
attributes[0][PropertySymbol.value]
155+
attribute[PropertySymbol.value]
156156
);
157-
attributeString += ' ' + attributes[0][PropertySymbol.name] + '="' + escapedValue + '"';
157+
attributeString += ' ' + attribute[PropertySymbol.name] + '="' + escapedValue + '"';
158158
}
159159

160160
return attributeString;

packages/happy-dom/src/nodes/document/Document.ts

+22-1
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ import SVGElementConfig from '../../config/SVGElementConfig.js';
5050
import StringUtility from '../../utilities/StringUtility.js';
5151
import HTMLParser from '../../html-parser/HTMLParser.js';
5252
import PreloadEntry from '../../fetch/preload/PreloadEntry.js';
53+
import DOMExceptionNameEnum from '../../exception/DOMExceptionNameEnum.js';
5354

5455
const PROCESSING_INSTRUCTION_TARGET_REGEXP = /^[a-z][a-z0-9-]+$/;
5556

@@ -1222,7 +1223,17 @@ export default class Document extends Node {
12221223
* @returns Attribute.
12231224
*/
12241225
public createAttribute(qualifiedName: string): Attr {
1225-
return this.createAttributeNS(null, StringUtility.asciiLowerCase(qualifiedName));
1226+
// We should use the NodeFactory and not the class constructor, so that owner document will be this document
1227+
const attribute = NodeFactory.createNode(this, this[PropertySymbol.window].Attr);
1228+
1229+
const name = StringUtility.asciiLowerCase(qualifiedName);
1230+
const parts = name.split(':');
1231+
1232+
attribute[PropertySymbol.name] = name;
1233+
attribute[PropertySymbol.localName] = parts[1] ?? name;
1234+
attribute[PropertySymbol.prefix] = parts[1] ? parts[0] : null;
1235+
1236+
return attribute;
12261237
}
12271238

12281239
/**
@@ -1237,11 +1248,21 @@ export default class Document extends Node {
12371248
const attribute = NodeFactory.createNode(this, this[PropertySymbol.window].Attr);
12381249

12391250
const parts = qualifiedName.split(':');
1251+
12401252
attribute[PropertySymbol.namespaceURI] = namespaceURI;
12411253
attribute[PropertySymbol.name] = qualifiedName;
12421254
attribute[PropertySymbol.localName] = parts[1] ?? qualifiedName;
12431255
attribute[PropertySymbol.prefix] = parts[1] ? parts[0] : null;
12441256

1257+
if (!namespaceURI && attribute[PropertySymbol.prefix]) {
1258+
throw new this[PropertySymbol.window].DOMException(
1259+
`Failed to execute 'createAttributeNS' on 'Document': The namespace URI provided ('${
1260+
namespaceURI || ''
1261+
}') is not valid for the qualified name provided ('${qualifiedName}').`,
1262+
DOMExceptionNameEnum.namespaceError
1263+
);
1264+
}
1265+
12451266
return attribute;
12461267
}
12471268

packages/happy-dom/src/nodes/element/Element.ts

+20-5
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import HTMLSerializer from '../../html-serializer/HTMLSerializer.js';
3333
import HTMLParser from '../../html-parser/HTMLParser.js';
3434
import IScrollToOptions from '../../window/IScrollToOptions.js';
3535
import { AttributeUtility } from '../../utilities/AttributeUtility.js';
36+
import DOMExceptionNameEnum from '../../exception/DOMExceptionNameEnum.js';
3637

3738
type InsertAdjacentPosition = 'beforebegin' | 'afterbegin' | 'beforeend' | 'afterend';
3839

@@ -545,9 +546,17 @@ export default class Element
545546
clone[PropertySymbol.shadowRoot][PropertySymbol.host] = clone;
546547
}
547548

548-
for (const item of this[PropertySymbol.attributes][PropertySymbol.namespaceItems].values()) {
549-
clone[PropertySymbol.attributes].setNamedItem(item.cloneNode());
550-
}
549+
clone[PropertySymbol.attributes][PropertySymbol.itemsByNamespaceURI] = new Map(
550+
this[PropertySymbol.attributes][PropertySymbol.itemsByNamespaceURI]
551+
);
552+
553+
clone[PropertySymbol.attributes][PropertySymbol.itemsByName] = new Map(
554+
this[PropertySymbol.attributes][PropertySymbol.itemsByName]
555+
);
556+
557+
clone[PropertySymbol.attributes][PropertySymbol.items] = new Map(
558+
this[PropertySymbol.attributes][PropertySymbol.items]
559+
);
551560

552561
return <Element>clone;
553562
}
@@ -704,6 +713,12 @@ export default class Element
704713
*/
705714
public setAttributeNS(namespaceURI: string, name: string, value: string): void {
706715
const attribute = this[PropertySymbol.ownerDocument].createAttributeNS(namespaceURI, name);
716+
if (!namespaceURI && attribute[PropertySymbol.prefix]) {
717+
throw new this[PropertySymbol.window].DOMException(
718+
`Failed to execute 'setAttributeNS' on 'Element': '' is an invalid namespace for attributes.`,
719+
DOMExceptionNameEnum.namespaceError
720+
);
721+
}
707722
attribute[PropertySymbol.value] = String(value);
708723
this[PropertySymbol.attributes].setNamedItemNS(attribute);
709724
}
@@ -715,7 +730,7 @@ export default class Element
715730
*/
716731
public getAttributeNames(): string[] {
717732
const names = [];
718-
for (const item of this[PropertySymbol.attributes][PropertySymbol.namespaceItems].values()) {
733+
for (const item of this[PropertySymbol.attributes][PropertySymbol.items].values()) {
719734
names.push(item[PropertySymbol.name]);
720735
}
721736
return names;
@@ -799,7 +814,7 @@ export default class Element
799814
* @returns "true" if the element has attributes.
800815
*/
801816
public hasAttributes(): boolean {
802-
return this[PropertySymbol.attributes][PropertySymbol.namespaceItems].size > 0;
817+
return this[PropertySymbol.attributes][PropertySymbol.items].size > 0;
803818
}
804819

805820
/**

packages/happy-dom/src/nodes/element/NamedNodeMap.ts

+42-27
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,14 @@ import StringUtility from '../../utilities/StringUtility.js';
1414
export default class NamedNodeMap {
1515
[index: number]: Attr;
1616

17-
// All items with the namespaceURI as prefix
18-
public [PropertySymbol.namespaceItems]: Map<string, Attr> = new Map();
17+
// Items by attribute namespaceURI
18+
public [PropertySymbol.itemsByNamespaceURI]: Map<string, Attr> = new Map();
1919

20-
// Items without namespaceURI as prefix, where the HTML namespace is the default namespace
21-
public [PropertySymbol.namedItems]: Map<string, Attr[]> = new Map();
20+
// Items by attribute name
21+
public [PropertySymbol.itemsByName]: Map<string, Attr[]> = new Map();
22+
23+
// All items
24+
public [PropertySymbol.items]: Map<string, Attr> = new Map();
2225

2326
public declare [PropertySymbol.ownerElement]: Element;
2427

@@ -37,7 +40,7 @@ export default class NamedNodeMap {
3740
* @returns Length.
3841
*/
3942
public get length(): number {
40-
return this[PropertySymbol.namespaceItems].size;
43+
return this[PropertySymbol.items].size;
4144
}
4245

4346
/**
@@ -64,7 +67,7 @@ export default class NamedNodeMap {
6467
* @returns Iterator.
6568
*/
6669
public [Symbol.iterator](): IterableIterator<Attr> {
67-
return this[PropertySymbol.namespaceItems].values();
70+
return this[PropertySymbol.items].values();
6871
}
6972

7073
/**
@@ -73,7 +76,7 @@ export default class NamedNodeMap {
7376
* @param index Index.
7477
*/
7578
public item(index: number): Attr | null {
76-
const items = Array.from(this[PropertySymbol.namespaceItems].values());
79+
const items = Array.from(this[PropertySymbol.items].values());
7780
return index >= 0 && items[index] ? items[index] : null;
7881
}
7982

@@ -91,9 +94,9 @@ export default class NamedNodeMap {
9194
PropertySymbol.contentType
9295
] === 'text/html'
9396
) {
94-
return this[PropertySymbol.namedItems].get(StringUtility.asciiLowerCase(name))?.[0] || null;
97+
return this[PropertySymbol.itemsByName].get(StringUtility.asciiLowerCase(name))?.[0] || null;
9598
}
96-
return this[PropertySymbol.namedItems].get(name)?.[0] || null;
99+
return this[PropertySymbol.itemsByName].get(name)?.[0] || null;
97100
}
98101

99102
/**
@@ -104,11 +107,17 @@ export default class NamedNodeMap {
104107
* @returns Item.
105108
*/
106109
public getNamedItemNS(namespace: string, localName: string): Attr | null {
107-
if (namespace === '') {
108-
namespace = null;
110+
const item = this[PropertySymbol.itemsByNamespaceURI].get(`${namespace || ''}:${localName}`);
111+
112+
// It seems like an item cant have a prefix without a namespaceURI
113+
// E.g. element.setAttribute('ns1:key', 'value1');
114+
// expect(element.attributes.getNamedItemNS(null, 'key')).toBeNull();
115+
116+
if (item && (!item[PropertySymbol.prefix] || item[PropertySymbol.namespaceURI])) {
117+
return item;
109118
}
110119

111-
return this[PropertySymbol.namespaceItems].get(`${namespace || ''}:${localName}`) || null;
120+
return null;
112121
}
113122

114123
/**
@@ -199,26 +208,29 @@ export default class NamedNodeMap {
199208
const replacedItem =
200209
this.getNamedItemNS(item[PropertySymbol.namespaceURI], item[PropertySymbol.localName]) ||
201210
null;
202-
203-
const namedItems = this[PropertySymbol.namedItems].get(item[PropertySymbol.name]);
211+
const itemsByName = this[PropertySymbol.itemsByName].get(item[PropertySymbol.name]);
204212

205213
if (replacedItem === item) {
206214
return item;
207215
}
208216

209-
this[PropertySymbol.namespaceItems].set(
217+
this[PropertySymbol.itemsByNamespaceURI].set(
210218
`${item[PropertySymbol.namespaceURI] || ''}:${item[PropertySymbol.localName]}`,
211219
item
212220
);
221+
this[PropertySymbol.items].set(
222+
`${item[PropertySymbol.namespaceURI] || ''}:${item[PropertySymbol.name]}`,
223+
item
224+
);
213225

214-
if (!namedItems?.length) {
215-
this[PropertySymbol.namedItems].set(item[PropertySymbol.name], [item]);
226+
if (!itemsByName?.length) {
227+
this[PropertySymbol.itemsByName].set(item[PropertySymbol.name], [item]);
216228
} else {
217-
const index = namedItems.indexOf(replacedItem);
229+
const index = itemsByName.indexOf(replacedItem);
218230
if (index !== -1) {
219-
namedItems.splice(index, 1);
231+
itemsByName.splice(index, 1);
220232
}
221-
namedItems.push(item);
233+
itemsByName.push(item);
222234
}
223235

224236
if (!ignoreListeners) {
@@ -237,19 +249,22 @@ export default class NamedNodeMap {
237249
public [PropertySymbol.removeNamedItem](item: Attr, ignoreListeners = false): void {
238250
item[PropertySymbol.ownerElement] = null;
239251

240-
this[PropertySymbol.namespaceItems].delete(
252+
this[PropertySymbol.itemsByNamespaceURI].delete(
241253
`${item[PropertySymbol.namespaceURI] || ''}:${item[PropertySymbol.localName]}`
242254
);
255+
this[PropertySymbol.items].delete(
256+
`${item[PropertySymbol.namespaceURI] || ''}:${item[PropertySymbol.name]}`
257+
);
243258

244-
const namedItems = this[PropertySymbol.namedItems].get(item[PropertySymbol.name]);
259+
const itemsByName = this[PropertySymbol.itemsByName].get(item[PropertySymbol.name]);
245260

246-
if (namedItems?.length) {
247-
const index = namedItems.indexOf(item);
261+
if (itemsByName?.length) {
262+
const index = itemsByName.indexOf(item);
248263
if (index !== -1) {
249-
namedItems.splice(index, 1);
264+
itemsByName.splice(index, 1);
250265
}
251-
if (!namedItems.length) {
252-
this[PropertySymbol.namedItems].delete(item[PropertySymbol.name]);
266+
if (!itemsByName.length) {
267+
this[PropertySymbol.itemsByName].delete(item[PropertySymbol.name]);
253268
}
254269
}
255270

packages/happy-dom/src/nodes/element/NamedNodeMapProxyFactory.ts

+22-17
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,15 @@ export default class NamedNodeMapProxyFactory {
2121
return new Proxy<NamedNodeMap>(namedNodeMap, {
2222
get: (target, property) => {
2323
if (property === 'length') {
24-
return namedNodeMap[PropertySymbol.namedItems].size;
24+
return namedNodeMap[PropertySymbol.items].size;
2525
}
2626
if (property in target || typeof property === 'symbol') {
2727
methodBinder.bind(property);
2828
return target[property];
2929
}
3030
const index = Number(property);
3131
if (!isNaN(index)) {
32-
return Array.from(namedNodeMap[PropertySymbol.namedItems].values())[index]?.[0];
32+
return target.item(index);
3333
}
3434
return target.getNamedItem(<string>property) || undefined;
3535
},
@@ -57,8 +57,8 @@ export default class NamedNodeMapProxyFactory {
5757
return true;
5858
},
5959
ownKeys(): string[] {
60-
const keys = Array.from(namedNodeMap[PropertySymbol.namedItems].keys());
61-
for (let i = 0, max = namedNodeMap[PropertySymbol.namedItems].size; i < max; i++) {
60+
const keys = Array.from(namedNodeMap[PropertySymbol.items].keys());
61+
for (let i = 0, max = namedNodeMap[PropertySymbol.items].size; i < max; i++) {
6262
keys.push(String(i));
6363
}
6464
return keys;
@@ -68,13 +68,13 @@ export default class NamedNodeMapProxyFactory {
6868
return false;
6969
}
7070

71-
if (property in target || namedNodeMap[PropertySymbol.namedItems].has(property)) {
71+
if (property in target || namedNodeMap[PropertySymbol.items].has(property)) {
7272
return true;
7373
}
7474

7575
const index = Number(property);
7676

77-
if (!isNaN(index) && index >= 0 && index < namedNodeMap[PropertySymbol.namedItems].size) {
77+
if (!isNaN(index) && index >= 0 && index < namedNodeMap[PropertySymbol.items].size) {
7878
return true;
7979
}
8080

@@ -96,21 +96,26 @@ export default class NamedNodeMapProxyFactory {
9696
}
9797

9898
const index = Number(property);
99-
100-
if (!isNaN(index) && index >= 0 && index < namedNodeMap[PropertySymbol.namedItems].size) {
101-
return {
102-
value: Array.from(namedNodeMap[PropertySymbol.namedItems].values())[index][0],
103-
writable: false,
104-
enumerable: true,
105-
configurable: true
106-
};
99+
if (!isNaN(index)) {
100+
if (index >= 0) {
101+
const itemByIndex = target.item(index);
102+
if (itemByIndex) {
103+
return {
104+
value: itemByIndex,
105+
writable: false,
106+
enumerable: true,
107+
configurable: true
108+
};
109+
}
110+
}
111+
return;
107112
}
108113

109-
const namedItems = namedNodeMap[PropertySymbol.namedItems].get(<string>property);
114+
const items = namedNodeMap[PropertySymbol.items].get(<string>property);
110115

111-
if (namedItems) {
116+
if (items) {
112117
return {
113-
value: namedItems[0],
118+
value: items,
114119
writable: false,
115120
enumerable: true,
116121
configurable: true

0 commit comments

Comments
 (0)