Skip to content
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
5 changes: 5 additions & 0 deletions .changeset/distinguish-dom-errors.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@remote-dom/polyfill': patch
---

Throw named DOM errors for invalid tree mutations and selector syntax.
16 changes: 9 additions & 7 deletions packages/polyfill/source/ChildNode.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {HOST, NEXT, PARENT, PREV} from './constants.ts';
import {performWithCustomElementReactions} from './custom-element-reactions.ts';
import {performHookEffects, type HookEffect} from './hook-effects.ts';
import {createDOMException} from './dom-exception.ts';
import type {ParentNode} from './ParentNode.ts';
import {Node} from './Node.ts';

Expand All @@ -21,7 +22,7 @@ export class ChildNode extends Node {
if (!parent) return;

return performChildNodeMutation((hookEffects) => {
validateNodesForInsertion(parent, staged);
validateInsertionNodes(parent, staged);

let next = this[NEXT];
while (next && staged.includes(next)) next = next[NEXT];
Expand All @@ -41,7 +42,7 @@ export class ChildNode extends Node {
if (!parent) return;

return performChildNodeMutation((hookEffects) => {
validateNodesForInsertion(parent, staged);
validateInsertionNodes(parent, staged);

let previous = this[PREV];
while (previous && staged.includes(previous)) previous = previous[PREV];
Expand All @@ -61,7 +62,7 @@ export class ChildNode extends Node {
if (!parent) return;

return performChildNodeMutation((hookEffects) => {
validateNodesForInsertion(parent, staged);
validateInsertionNodes(parent, staged);

let next = this[NEXT];
while (next && staged.includes(next)) next = next[NEXT];
Expand Down Expand Up @@ -89,7 +90,7 @@ function performChildNodeMutation(
});
}

function stageNodes(nodes: (Node | string)[]) {
export function stageNodes(nodes: (Node | string)[]) {
return nodes.map((node) => (node instanceof Node ? node : String(node)));
}

Expand All @@ -99,7 +100,7 @@ export function toNode(parent: ParentNode, node: Node | any) {
return ownerDocument.createTextNode(String(node));
}

function validateNodesForInsertion(
export function validateInsertionNodes(
parent: ParentNode,
nodes: (Node | string)[],
) {
Expand All @@ -109,8 +110,9 @@ function validateNodesForInsertion(
let ancestor: Node | null = parent;
while (ancestor) {
if (ancestor === node) {
throw Error(
'cannot insert a node into itself or one of its descendants',
throw createDOMException(
'Cannot insert a node into itself or one of its descendants',
'HierarchyRequestError',
);
}
ancestor = ancestor[PARENT] ?? ancestor[HOST];
Expand Down
8 changes: 5 additions & 3 deletions packages/polyfill/source/CustomElementRegistry.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import {createDOMException} from './dom-exception.ts';

const VALID_CUSTOM_ELEMENT_NAME =
/^[a-z][^A-Z\u0000\t\n\f\r />]*-[^A-Z\u0000\t\n\f\r />]*$/u;

Expand All @@ -20,7 +22,7 @@ function isValidCustomElementName(name: string) {
}

function createInvalidCustomElementNameError(name: string) {
return new DOMException(
return createDOMException(
`Invalid custom element name: "${name}"`,
'SyntaxError',
);
Expand All @@ -45,14 +47,14 @@ export class CustomElementRegistryImplementation
}

if (this.registry.has(name)) {
throw new DOMException(
throw createDOMException(
`A custom element named "${name}" has already been defined`,
'NotSupportedError',
);
}

if (this.getName(Constructor) != null) {
throw new DOMException(
throw createDOMException(
'This constructor has already been registered in this custom element registry',
'NotSupportedError',
);
Expand Down
7 changes: 5 additions & 2 deletions packages/polyfill/source/Document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ import {
adoptNodes,
cloneNode,
collectAdoptionSnapshot,
createNotSupportedError,
getElementById as findElementById,
getElementsByClassName as findElementsByClassName,
getElementsByTagName as findElementsByTagName,
Expand All @@ -40,6 +39,7 @@ import {HTMLBodyElement} from './HTMLBodyElement.ts';
import {HTMLHeadElement} from './HTMLHeadElement.ts';
import {HTMLHtmlElement} from './HTMLHtmlElement.ts';
import {performWithCustomElementReactions} from './custom-element-reactions.ts';
import {createDOMException} from './dom-exception.ts';

export class Document extends ParentNode {
nodeType: NodeType = NODE_TYPE_DOCUMENT;
Expand Down Expand Up @@ -133,7 +133,10 @@ export class Document extends ParentNode {

importNode(node: Node, deep?: boolean) {
if (node.nodeType === NODE_TYPE_DOCUMENT) {
throw createNotSupportedError('Cannot import a document node');
throw createDOMException(
'Cannot import a document node',
'NotSupportedError',
);
}

return cloneNode(node, deep, this);
Expand Down
15 changes: 3 additions & 12 deletions packages/polyfill/source/EventTarget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
OWNER_DOCUMENT,
STOP_IMMEDIATE_PROPAGATION,
} from './constants.ts';
import {createDOMException} from './dom-exception.ts';
import {
EVENT_PHASE_NONE,
EVENT_PHASE_BUBBLING,
Expand Down Expand Up @@ -165,8 +166,9 @@ export class EventTarget {

dispatchEvent(event: Event) {
if (event[DISPATCHING]) {
throw createInvalidStateError(
throw createDOMException(
`Failed to execute 'dispatchEvent' on 'EventTarget': The event is already being dispatched.`,
'InvalidStateError',
);
}

Expand Down Expand Up @@ -280,14 +282,3 @@ function removeListenerRegistration(
registration.capture,
);
}

function createInvalidStateError(message: string) {
const DOMExceptionConstructor = globalThis.DOMException;
if (typeof DOMExceptionConstructor === 'function') {
return new DOMExceptionConstructor(message, 'InvalidStateError');
}

const error = new Error(message);
error.name = 'InvalidStateError';
return error;
}
6 changes: 3 additions & 3 deletions packages/polyfill/source/NamedNodeMap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
} from './MutationObserver.ts';
import {performWithCustomElementReactions} from './custom-element-reactions.ts';
import {enqueueAttributeReaction} from './attribute-reactions.ts';
import {createDOMException} from './dom-exception.ts';

export class NamedNodeMap {
[CHILD]: Attr | null = null;
Expand Down Expand Up @@ -155,11 +156,10 @@ export class NamedNodeMap {
const currentOwner = attr[OWNER_ELEMENT];

if (currentOwner != null && currentOwner !== ownerElement) {
const error = new Error(
throw createDOMException(
'The attribute is already in use by another element.',
'InUseAttributeError',
);
error.name = 'InUseAttributeError';
throw error;
}

let old = null;
Expand Down
55 changes: 31 additions & 24 deletions packages/polyfill/source/ParentNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import {
NEXT,
PREV,
PARENT,
HOST,
OWNER_DOCUMENT,
NODE_TYPE_DOCUMENT_FRAGMENT,
NODE_TYPE_ELEMENT,
Expand All @@ -17,7 +16,9 @@ import {
INSERT_NODE,
PREFLIGHT_INSERTIONS,
REPLACE_NODE,
stageNodes,
toNode,
validateInsertionNodes,
} from './ChildNode.ts';
import {NodeList} from './NodeList.ts';
import {querySelectorAll, querySelector} from './selectors.ts';
Expand All @@ -41,6 +42,7 @@ import {
performHookEffects,
type HookEffect,
} from './hook-effects.ts';
import {createDOMException} from './dom-exception.ts';

interface PreparedInsertionRoot {
node: Node;
Expand Down Expand Up @@ -76,32 +78,36 @@ export class ParentNode extends ChildNode {

append(...nodes: (Node | string)[]) {
return performWithCustomElementReactions(() => {
for (const child of nodes) {
if (child == null) continue;
const staged = stageNodes(nodes.filter((node) => node != null));
if (staged.length > 1) validateInsertionNodes(this, staged);
for (const child of staged) {
this[INSERT_NODE](toNode(this, child), null);
}
});
}

prepend(...nodes: (Node | string)[]) {
return performWithCustomElementReactions(() => {
const staged = stageNodes(nodes.filter((node) => node != null));
if (staged.length > 1) validateInsertionNodes(this, staged);
const before = this.firstChild;
for (const child of nodes) {
if (child == null) continue;
for (const child of staged) {
this[INSERT_NODE](toNode(this, child), before);
}
});
}

replaceChildren(...nodes: (Node | string)[]) {
return performWithCustomElementReactions(() => {
const staged = stageNodes(nodes.filter((node) => node != null));
validateInsertionNodes(this, staged);

let child;
while ((child = this.firstChild)) {
this.removeChildImmediately(child);
}
for (const child of nodes) {
if (child == null) continue;
this[INSERT_NODE](toNode(this, child), null);
for (const node of staged) {
this[INSERT_NODE](toNode(this, node), null);
}
});
}
Expand All @@ -114,8 +120,12 @@ export class ParentNode extends ChildNode {
}

private removeChildImmediately(child: Node) {
if (child.parentNode !== this) throw Error(`not a child of this node`);

if (child.parentNode !== this) {
throw createDOMException(
'The node is not a child of this node',
'NotFoundError',
);
}
const disconnectedNodes = this[IS_CONNECTED]
? selfAndDescendants(child)
: undefined;
Expand All @@ -142,14 +152,16 @@ export class ParentNode extends ChildNode {
}

[REPLACE_NODE](newChild: Node, oldChild: Node, hookEffects?: HookEffect[]) {
validateInsertionNodes(this, [newChild]);
if (oldChild.parentNode !== this) {
throw Error('reference node is not a child of this parent');
throw createDOMException(
'The reference node is not a child of this parent',
'NotFoundError',
);
}

const previous = oldChild[PREV];
const next = oldChild[NEXT];
this.validateInsertion(newChild, next);

const insertion = this.prepareInsertion(newChild);
const removedNodes = this[IS_CONNECTED]
? selfAndDescendants(oldChild)
Expand Down Expand Up @@ -217,18 +229,13 @@ export class ParentNode extends ChildNode {
}

private validateInsertion(child: Node, before: Node | null) {
if (before && before.parentNode !== this) {
throw Error('reference node is not a child of this parent');
}
validateInsertionNodes(this, [child]);

let ancestor: Node | null = this;
while (ancestor) {
if (ancestor === child) {
throw Error(
'cannot insert a node into itself or one of its descendants',
);
}
ancestor = ancestor[PARENT] ?? ancestor[HOST];
if (before && before.parentNode !== this) {
throw createDOMException(
'The reference node is not a child of this parent',
'NotFoundError',
);
}
}

Expand Down
14 changes: 14 additions & 0 deletions packages/polyfill/source/dom-exception.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export function createDOMException(
message: string,
name: string,
): DOMException {
const DOMExceptionConstructor = globalThis.DOMException;

if (typeof DOMExceptionConstructor === 'function') {
return new DOMExceptionConstructor(message, name);
}

const error = new Error(message);
error.name = name;
return error as DOMException;
}
11 changes: 6 additions & 5 deletions packages/polyfill/source/names.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
XMLNS_NAMESPACE,
type NamespaceURI,
} from './constants.ts';
import {createDOMException} from './dom-exception.ts';

const VALID_ELEMENT_LOCAL_NAME =
/^(?:[A-Za-z][^\0\t\n\f\r\u0020/>]*|[:_\u0080-\u{10FFFF}][A-Za-z0-9-.:_\u0080-\u{10FFFF}]*)$/u;
Expand Down Expand Up @@ -51,14 +52,14 @@ export function validateAndExtractQualifiedName(
}

if (prefix != null && namespace == null) {
throw new DOMException(
throw createDOMException(
`A namespace is required for the prefix in "${qualifiedName}"`,
'NamespaceError',
);
}

if (prefix === 'xml' && namespace !== XML_NAMESPACE) {
throw new DOMException(
throw createDOMException(
`The xml prefix requires the XML namespace`,
'NamespaceError',
);
Expand All @@ -68,7 +69,7 @@ export function validateAndExtractQualifiedName(
(qualifiedName === 'xmlns' || prefix === 'xmlns') &&
namespace !== XMLNS_NAMESPACE
) {
throw new DOMException(
throw createDOMException(
`The xmlns name requires the XMLNS namespace`,
'NamespaceError',
);
Expand All @@ -79,7 +80,7 @@ export function validateAndExtractQualifiedName(
qualifiedName !== 'xmlns' &&
prefix !== 'xmlns'
) {
throw new DOMException(
throw createDOMException(
`The XMLNS namespace requires the xmlns name or prefix`,
'NamespaceError',
);
Expand All @@ -89,5 +90,5 @@ export function validateAndExtractQualifiedName(
}

function throwInvalidCharacterError(name: string): never {
throw new DOMException(`Invalid name: "${name}"`, 'InvalidCharacterError');
throw createDOMException(`Invalid name: "${name}"`, 'InvalidCharacterError');
}
Loading
Loading