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: Properly check nodes before replacement #457

Merged
merged 1 commit into from Nov 3, 2022
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
199 changes: 173 additions & 26 deletions lib/dom.js
Expand Up @@ -514,7 +514,7 @@ Node.prototype = {
},
replaceChild: function (newChild, oldChild) {
//raises
this.insertBefore(newChild, oldChild);
_insertBefore(this, newChild, oldChild, assertPreReplacementValidityInDocument);
if (oldChild) {
this.removeChild(oldChild);
}
Expand Down Expand Up @@ -831,64 +831,200 @@ function isElementInsertionPossible(doc, child) {
var docTypeNode = find(parentChildNodes, isDocTypeNode);
return !(child && docTypeNode && parentChildNodes.indexOf(docTypeNode) > parentChildNodes.indexOf(child));
}

/**
* Check if en element node can be inserted before `child`, or at the end if child is falsy,
* according to the presence and position of a doctype node on the same level.
*
* @param {Node} doc The document node
* @param {Node} child the node that would become the nextSibling if the element would be inserted
* @returns {boolean} `true` if an element can be inserted before child
* @private
* https://dom.spec.whatwg.org/#concept-node-ensure-pre-insertion-validity
*/
function isElementReplacementPossible(doc, child) {
var parentChildNodes = doc.childNodes || [];

function hasElementChildThatIsNotChild(node) {
return isElementNode(node) && node !== child;
}

if (find(parentChildNodes, hasElementChildThatIsNotChild)) {
return false;
}
var docTypeNode = find(parentChildNodes, isDocTypeNode);
return !(child && docTypeNode && parentChildNodes.indexOf(docTypeNode) > parentChildNodes.indexOf(child));
}

/**
* @private
* Steps 1-5 of the checks before inserting and before replacing a child are the same.
*
* @param {Node} parent the parent node to insert `node` into
* @param {Node} node the node to insert
* @param {Node=} child the node that should become the `nextSibling` of `node`
* @returns {Node}
* @throws DOMException for several node combinations that would create a DOM that is not well-formed.
* @throws DOMException if `child` is provided but is not a child of `parent`.
* @see https://dom.spec.whatwg.org/#concept-node-ensure-pre-insertion-validity
* @see https://dom.spec.whatwg.org/#concept-node-replace
*/
function _insertBefore(parent, node, child) {
function assertPreInsertionValidity1to5(parent, node, child) {
// 1. If `parent` is not a Document, DocumentFragment, or Element node, then throw a "HierarchyRequestError" DOMException.
if (!hasValidParentNodeType(parent)) {
throw new DOMException(HIERARCHY_REQUEST_ERR, 'Unexpected parent node type ' + parent.nodeType);
}
// 2. If `node` is a host-including inclusive ancestor of `parent`, then throw a "HierarchyRequestError" DOMException.
// not implemented!
// 3. If `child` is non-null and its parent is not `parent`, then throw a "NotFoundError" DOMException.
if (child && child.parentNode !== parent) {
throw new DOMException(NOT_FOUND_ERR, 'child not in parent');
}
if (
// 4. If `node` is not a DocumentFragment, DocumentType, Element, or CharacterData node, then throw a "HierarchyRequestError" DOMException.
!hasInsertableNodeType(node) ||
// 5. If either `node` is a Text node and `parent` is a document,
// the sax parser currently adds top level text nodes, this will be fixed in 0.9.0
// || (node.nodeType === Node.TEXT_NODE && parent.nodeType === Node.DOCUMENT_NODE)
// or `node` is a doctype and `parent` is not a document, then throw a "HierarchyRequestError" DOMException.
(isDocTypeNode(node) && parent.nodeType !== Node.DOCUMENT_NODE)
) {
throw new DOMException(
HIERARCHY_REQUEST_ERR,
'Unexpected node type ' + node.nodeType + ' for parent node type ' + parent.nodeType
);
}
}

/**
* @private
* Step 6 of the checks before inserting and before replacing a child are different.
*
* @param {Document} parent the parent node to insert `node` into
* @param {Node} node the node to insert
* @param {Node | undefined} child the node that should become the `nextSibling` of `node`
* @returns {Node}
* @throws DOMException for several node combinations that would create a DOM that is not well-formed.
* @throws DOMException if `child` is provided but is not a child of `parent`.
* @see https://dom.spec.whatwg.org/#concept-node-ensure-pre-insertion-validity
* @see https://dom.spec.whatwg.org/#concept-node-replace
*/
function assertPreInsertionValidityInDocument(parent, node, child) {
var parentChildNodes = parent.childNodes || [];
var nodeChildNodes = node.childNodes || [];
if (parent.nodeType === Node.DOCUMENT_NODE) {
if (node.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
var nodeChildElements = nodeChildNodes.filter(isElementNode);
if (nodeChildElements.length > 1 || find(nodeChildNodes, isTextNode)) {
throw new DOMException(HIERARCHY_REQUEST_ERR, 'More than one element or text in fragment');
}
if (nodeChildElements.length === 1 && !isElementInsertionPossible(parent, child)) {
throw new DOMException(HIERARCHY_REQUEST_ERR, 'Element in fragment can not be inserted before doctype');
}

// DocumentFragment
if (node.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
var nodeChildElements = nodeChildNodes.filter(isElementNode);
// If node has more than one element child or has a Text node child.
if (nodeChildElements.length > 1 || find(nodeChildNodes, isTextNode)) {
throw new DOMException(HIERARCHY_REQUEST_ERR, 'More than one element or text in fragment');
}
if (isElementNode(node)) {
if (find(parentChildNodes, isElementNode) || !isElementInsertionPossible(parent, child)) {
throw new DOMException(HIERARCHY_REQUEST_ERR, 'Only one element can be added and only after doctype');
}
// Otherwise, if `node` has one element child and either `parent` has an element child,
// `child` is a doctype, or `child` is non-null and a doctype is following `child`.
if (nodeChildElements.length === 1 && !isElementInsertionPossible(parent, child)) {
throw new DOMException(HIERARCHY_REQUEST_ERR, 'Element in fragment can not be inserted before doctype');
}
if (isDocTypeNode(node)) {
if (find(parentChildNodes, isDocTypeNode)) {
throw new DOMException(HIERARCHY_REQUEST_ERR, 'Only one doctype is allowed');
}
var parentElementChild = find(parentChildNodes, isElementNode);
if (child && parentChildNodes.indexOf(parentElementChild) < parentChildNodes.indexOf(child)) {
throw new DOMException(HIERARCHY_REQUEST_ERR, 'Doctype can only be inserted before an element');
}
if (!child && parentElementChild) {
throw new DOMException(HIERARCHY_REQUEST_ERR, 'Doctype can not be appended since element is present');
}
}
// Element
if (isElementNode(node)) {
// `parent` has an element child, `child` is a doctype,
// or `child` is non-null and a doctype is following `child`.
if (!isElementInsertionPossible(parent, child)) {
throw new DOMException(HIERARCHY_REQUEST_ERR, 'Only one element can be added and only after doctype');
}
}
// DocumentType
if (isDocTypeNode(node)) {
// `parent` has a doctype child,
if (find(parentChildNodes, isDocTypeNode)) {
throw new DOMException(HIERARCHY_REQUEST_ERR, 'Only one doctype is allowed');
}
var parentElementChild = find(parentChildNodes, isElementNode);
// `child` is non-null and an element is preceding `child`,
if (child && parentChildNodes.indexOf(parentElementChild) < parentChildNodes.indexOf(child)) {
throw new DOMException(HIERARCHY_REQUEST_ERR, 'Doctype can only be inserted before an element');
}
// or `child` is null and `parent` has an element child.
if (!child && parentElementChild) {
throw new DOMException(HIERARCHY_REQUEST_ERR, 'Doctype can not be appended since element is present');
}
}
}

/**
* @private
* Step 6 of the checks before inserting and before replacing a child are different.
*
* @param {Document} parent the parent node to insert `node` into
* @param {Node} node the node to insert
* @param {Node | undefined} child the node that should become the `nextSibling` of `node`
* @returns {Node}
* @throws DOMException for several node combinations that would create a DOM that is not well-formed.
* @throws DOMException if `child` is provided but is not a child of `parent`.
* @see https://dom.spec.whatwg.org/#concept-node-ensure-pre-insertion-validity
* @see https://dom.spec.whatwg.org/#concept-node-replace
*/
function assertPreReplacementValidityInDocument(parent, node, child) {
var parentChildNodes = parent.childNodes || [];
var nodeChildNodes = node.childNodes || [];

// DocumentFragment
if (node.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
var nodeChildElements = nodeChildNodes.filter(isElementNode);
// If `node` has more than one element child or has a Text node child.
if (nodeChildElements.length > 1 || find(nodeChildNodes, isTextNode)) {
throw new DOMException(HIERARCHY_REQUEST_ERR, 'More than one element or text in fragment');
}
// Otherwise, if `node` has one element child and either `parent` has an element child that is not `child` or a doctype is following `child`.
if (nodeChildElements.length === 1 && !isElementReplacementPossible(parent, child)) {
throw new DOMException(HIERARCHY_REQUEST_ERR, 'Element in fragment can not be inserted before doctype');
}
}
// Element
if (isElementNode(node)) {
// `parent` has an element child that is not `child` or a doctype is following `child`.
if (!isElementReplacementPossible(parent, child)) {
throw new DOMException(HIERARCHY_REQUEST_ERR, 'Only one element can be added and only after doctype');
}
}
// DocumentType
if (isDocTypeNode(node)) {
function hasDoctypeChildThatIsNotChild(node) {
return isDocTypeNode(node) && node !== child;
}

// `parent` has a doctype child that is not `child`,
if (find(parentChildNodes, hasDoctypeChildThatIsNotChild)) {
throw new DOMException(HIERARCHY_REQUEST_ERR, 'Only one doctype is allowed');
}
var parentElementChild = find(parentChildNodes, isElementNode);
// or an element is preceding `child`.
if (child && parentChildNodes.indexOf(parentElementChild) < parentChildNodes.indexOf(child)) {
throw new DOMException(HIERARCHY_REQUEST_ERR, 'Doctype can only be inserted before an element');
}
}
}

/**
* @private
* @param {Node} parent the parent node to insert `node` into
* @param {Node} node the node to insert
* @param {Node=} child the node that should become the `nextSibling` of `node`
* @returns {Node}
* @throws DOMException for several node combinations that would create a DOM that is not well-formed.
* @throws DOMException if `child` is provided but is not a child of `parent`.
* @see https://dom.spec.whatwg.org/#concept-node-ensure-pre-insertion-validity
*/
function _insertBefore(parent, node, child, _inDocumentAssertion) {
// To ensure pre-insertion validity of a node into a parent before a child, run these steps:
assertPreInsertionValidity1to5(parent, node, child);

// If parent is a document, and any of the statements below, switched on the interface node implements,
// are true, then throw a "HierarchyRequestError" DOMException.
if (parent.nodeType === Node.DOCUMENT_NODE) {
(_inDocumentAssertion || assertPreInsertionValidityInDocument)(parent, node, child);
}

var cp = node.parentNode;
if (cp) {
Expand Down Expand Up @@ -1001,6 +1137,17 @@ Document.prototype = {
}
return _removeChild(this, oldChild);
},
replaceChild: function (newChild, oldChild) {
//raises
_insertBefore(this, newChild, oldChild, assertPreReplacementValidityInDocument);
newChild.ownerDocument = this;
if (oldChild) {
this.removeChild(oldChild);
}
if (isElementNode(newChild)) {
this.documentElement = newChild;
}
},
// Introduced in DOM Level 2:
importNode: function (importedNode, deep) {
return importNode(this, importedNode, deep);
Expand Down
29 changes: 29 additions & 0 deletions test/dom/document.test.js
Expand Up @@ -230,4 +230,33 @@ describe('Document.prototype', () => {
expect(root.parentNode).toBeNull();
});
});
describe('replaceChild', () => {
it('should remove the only element and add the new one', () => {
const doc = new DOMImplementation().createDocument('', 'xml');
const initialFirstChild = doc.firstChild;
const replacement = doc.createElement('replaced');

doc.replaceChild(replacement, doc.firstChild);

expect(doc.childNodes).toHaveLength(1);
expect(initialFirstChild.parentNode).toBeNull();
expect(doc.documentElement.name).toBe(replacement.name);
});
});
describe('removeChild', () => {
it('should remove all connections to node', () => {
const doc = new DOMImplementation().createDocument('', 'xml');
doc.insertBefore(doc.createComment('just a comment'), doc.firstChild);
expect(doc.childNodes).toHaveLength(2);
const initialElement = doc.firstChild;

doc.removeChild(initialElement);

// expect(doc.documentElement).toBeNull();
expect(initialElement.parentNode).toBeNull();
expect(initialElement.nextSibling).toBeNull();
// expect(initialElement.previousSibling).toBeNull();
expect(doc.childNodes).toHaveLength(1);
});
});
});