diff --git a/NEWS b/NEWS index 811eeb187dd6e..54ebe4b5e52e7 100644 --- a/NEWS +++ b/NEWS @@ -6,4 +6,7 @@ Core: . Fixed bug GH-12073 (Segfault when freeing incompletely initialized closures). (ilutov) +DOM: + . Added DOMNode::compareDocumentPosition(). (nielsdos) + <<< NOTE: Insert NEWS from last stable release here prior to actual release! >>> diff --git a/UPGRADING b/UPGRADING index 738c11675a902..73f50ea5d87d6 100644 --- a/UPGRADING +++ b/UPGRADING @@ -19,10 +19,25 @@ PHP 8.4 UPGRADE NOTES 1. Backward Incompatible Changes ======================================== +- DOM: + . New methods and constants were added to some DOM classes. If you inherit + from these and you happen to have a method or property with the same name, + you might encounter errors if the declaration is incompatible. + Consult sections 2. New Features and 6. New Functions for a list of + newly implemented methods and constants. + ======================================== 2. New Features ======================================== +- DOM: + . Added constant DOMNode::DOCUMENT_POSITION_DISCONNECTED. + . Added constant DOMNode::DOCUMENT_POSITION_PRECEDING. + . Added constant DOMNode::DOCUMENT_POSITION_FOLLOWING. + . Added constant DOMNode::DOCUMENT_POSITION_CONTAINS. + . Added constant DOMNode::DOCUMENT_POSITION_CONTAINED_BY. + . Added constant DOMNode::DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC. + ======================================== 3. Changes in SAPI modules ======================================== @@ -39,6 +54,9 @@ PHP 8.4 UPGRADE NOTES 6. New Functions ======================================== +- DOM: + . Added DOMNode::compareDocumentPosition(). + ======================================== 7. New Classes and Interfaces ======================================== diff --git a/ext/dom/node.c b/ext/dom/node.c index 7064fa10f0e41..144181c67915e 100644 --- a/ext/dom/node.c +++ b/ext/dom/node.c @@ -1518,6 +1518,15 @@ static bool php_dom_node_is_equal_node(const xmlNode *this, const xmlNode *other PHP_DOM_DEFINE_LIST_EQUALITY_HELPER(xmlNode) PHP_DOM_DEFINE_LIST_EQUALITY_HELPER(xmlNs) +static bool php_dom_is_equal_attr(const xmlAttr *this_attr, const xmlAttr *other_attr) +{ + ZEND_ASSERT(this_attr != NULL); + ZEND_ASSERT(other_attr != NULL); + return xmlStrEqual(this_attr->name, other_attr->name) + && php_dom_node_is_ns_uri_equal((const xmlNode *) this_attr, (const xmlNode *) other_attr) + && php_dom_node_is_content_equal((const xmlNode *) this_attr, (const xmlNode *) other_attr); +} + static bool php_dom_node_is_equal_node(const xmlNode *this, const xmlNode *other) { ZEND_ASSERT(this != NULL); @@ -1552,9 +1561,7 @@ static bool php_dom_node_is_equal_node(const xmlNode *this, const xmlNode *other } else if (this->type == XML_ATTRIBUTE_NODE) { const xmlAttr *this_attr = (const xmlAttr *) this; const xmlAttr *other_attr = (const xmlAttr *) other; - return xmlStrEqual(this_attr->name, other_attr->name) - && php_dom_node_is_ns_uri_equal(this, other) - && php_dom_node_is_content_equal(this, other); + return php_dom_is_equal_attr(this_attr, other_attr); } else if (this->type == XML_ENTITY_REF_NODE) { return xmlStrEqual(this->name, other->name); } else if (this->type == XML_ENTITY_DECL || this->type == XML_NOTATION_NODE || this->type == XML_ENTITY_NODE) { @@ -2030,4 +2037,170 @@ PHP_METHOD(DOMNode, getRootNode) } /* }}} */ +/* {{{ URL: https://dom.spec.whatwg.org/#dom-node-comparedocumentposition (last check date 2023-07-24) +Since: +*/ + +#define DOCUMENT_POSITION_DISCONNECTED 0x01 +#define DOCUMENT_POSITION_PRECEDING 0x02 +#define DOCUMENT_POSITION_FOLLOWING 0x04 +#define DOCUMENT_POSITION_CONTAINS 0x08 +#define DOCUMENT_POSITION_CONTAINED_BY 0x10 +#define DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC 0x20 + +PHP_METHOD(DOMNode, compareDocumentPosition) +{ + zval *id, *node_zval; + xmlNodePtr other, this; + dom_object *this_intern, *other_intern; + + if (zend_parse_parameters(ZEND_NUM_ARGS(), "O", &node_zval, dom_node_class_entry) == FAILURE) { + RETURN_THROWS(); + } + + DOM_GET_THIS_OBJ(this, id, xmlNodePtr, this_intern); + DOM_GET_OBJ(other, node_zval, xmlNodePtr, other_intern); + + /* Step 1 */ + if (this == other) { + RETURN_LONG(0); + } + + /* Step 2 */ + xmlNodePtr node1 = other; + xmlNodePtr node2 = this; + + /* Step 3 */ + xmlNodePtr attr1 = NULL; + xmlNodePtr attr2 = NULL; + + /* Step 4 */ + if (node1->type == XML_ATTRIBUTE_NODE) { + attr1 = node1; + node1 = attr1->parent; + } + + /* Step 5 */ + if (node2->type == XML_ATTRIBUTE_NODE) { + /* 5.1 */ + attr2 = node2; + node2 = attr2->parent; + + /* 5.2 */ + if (attr1 != NULL && node1 != NULL && node2 == node1) { + for (const xmlAttr *attr = node2->properties; attr != NULL; attr = attr->next) { + if (php_dom_is_equal_attr(attr, (const xmlAttr *) attr1)) { + RETURN_LONG(DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC | DOCUMENT_POSITION_PRECEDING); + } else if (php_dom_is_equal_attr(attr, (const xmlAttr *) attr2)) { + RETURN_LONG(DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC | DOCUMENT_POSITION_FOLLOWING); + } + } + } + } + + /* Step 6 */ + /* We first check the first condition, + * and as we need the root later anyway we'll cache the root and perform the root check after this if. */ + if (node1 == NULL || node2 == NULL) { + goto disconnected; + } + bool node2_is_ancestor_of_node1 = false; + size_t node1_depth = 0; + xmlNodePtr node1_root = node1; + while (node1_root->parent) { + node1_root = node1_root->parent; + if (node1_root == node2) { + node2_is_ancestor_of_node1 = true; + } + node1_depth++; + } + bool node1_is_ancestor_of_node2 = false; + size_t node2_depth = 0; + xmlNodePtr node2_root = node2; + while (node2_root->parent) { + node2_root = node2_root->parent; + if (node2_root == node1) { + node1_is_ancestor_of_node2 = true; + } + node2_depth++; + } + /* Second condition from step 6 */ + if (node1_root != node2_root) { + goto disconnected; + } + + /* Step 7 */ + if ((node1_is_ancestor_of_node2 && attr1 == NULL) || (node1 == node2 && attr2 != NULL)) { + RETURN_LONG(DOCUMENT_POSITION_CONTAINS | DOCUMENT_POSITION_PRECEDING); + } + + /* Step 8 */ + if ((node2_is_ancestor_of_node1 && attr2 == NULL) || (node1 == node2 && attr1 != NULL)) { + RETURN_LONG(DOCUMENT_POSITION_CONTAINED_BY | DOCUMENT_POSITION_FOLLOWING); + } + + /* Special case: comparing children and attributes. + * They belong to a different tree and are therefore hard to compare, but spec demands attributes to precede children + * according to the pre-order depth-first search ordering. + * Because their tree is different, the node parents only meet at the common element instead of earlier. + * Therefore, it seems that one is the ancestor of the other. */ + if (node1_is_ancestor_of_node2) { + ZEND_ASSERT(attr1 != NULL); /* Would've been handled in step 7 otherwise */ + RETURN_LONG(DOCUMENT_POSITION_PRECEDING); + } else if (node2_is_ancestor_of_node1) { + ZEND_ASSERT(attr2 != NULL); /* Would've been handled in step 8 otherwise */ + RETURN_LONG(DOCUMENT_POSITION_FOLLOWING); + } + + /* Step 9 */ + + /* We'll use the following strategy (which was already prepared during step 6) to implement this efficiently: + * 1. Move nodes upwards such that they are at the same depth. + * 2. Then we move both nodes upwards simultaneously until their parents are equal. + * 3. If we then move node1 to the next entry repeatedly and we encounter node2, + * then we know node1 precedes node2. Otherwise, node2 must precede node1. */ + /* 1. */ + if (node1_depth > node2_depth) { + do { + node1 = node1->parent; + node1_depth--; + } while (node1_depth > node2_depth); + } else if (node2_depth > node1_depth) { + do { + node2 = node2->parent; + node2_depth--; + } while (node2_depth > node1_depth); + } + /* 2. */ + while (node1->parent != node2->parent) { + node1 = node1->parent; + node2 = node2->parent; + } + /* 3. */ + ZEND_ASSERT(node1 != node2); + ZEND_ASSERT(node1 != NULL); + ZEND_ASSERT(node2 != NULL); + do { + node1 = node1->next; + if (node1 == node2) { + RETURN_LONG(DOCUMENT_POSITION_PRECEDING); + } + } while (node1 != NULL); + + /* Step 10 */ + RETURN_LONG(DOCUMENT_POSITION_FOLLOWING); + +disconnected:; + zend_long ordering; + if (UNEXPECTED(node1 == node2)) { + /* Degenerate case, they're both NULL, but the ordering must be consistent... */ + ZEND_ASSERT(node1 == NULL); + ordering = other_intern < this_intern ? DOCUMENT_POSITION_PRECEDING : DOCUMENT_POSITION_FOLLOWING; + } else { + ordering = node1 < node2 ? DOCUMENT_POSITION_PRECEDING : DOCUMENT_POSITION_FOLLOWING; + } + RETURN_LONG(DOCUMENT_POSITION_DISCONNECTED | DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC | ordering); +} +/* }}} */ + #endif diff --git a/ext/dom/php_dom.stub.php b/ext/dom/php_dom.stub.php index 7316f8426698b..bc1033b29df74 100644 --- a/ext/dom/php_dom.stub.php +++ b/ext/dom/php_dom.stub.php @@ -297,6 +297,13 @@ public function replaceWith(...$nodes): void; /** @not-serializable */ class DOMNode { + public const int DOCUMENT_POSITION_DISCONNECTED = 0x01; + public const int DOCUMENT_POSITION_PRECEDING = 0x02; + public const int DOCUMENT_POSITION_FOLLOWING = 0x04; + public const int DOCUMENT_POSITION_CONTAINS = 0x08; + public const int DOCUMENT_POSITION_CONTAINED_BY = 0x10; + public const int DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC = 0x20; + /** @readonly */ public string $nodeName; @@ -404,6 +411,8 @@ public function replaceChild(DOMNode $node, DOMNode $child) {} public function contains(DOMNode|DOMNameSpaceNode|null $other): bool {} public function getRootNode(?array $options = null): DOMNode {} + + public function compareDocumentPosition(DOMNode $other): int {} } /** @not-serializable */ diff --git a/ext/dom/php_dom_arginfo.h b/ext/dom/php_dom_arginfo.h index 76d8b0af2b0b5..1b7fc063fa2d6 100644 --- a/ext/dom/php_dom_arginfo.h +++ b/ext/dom/php_dom_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit the .stub.php file instead. - * Stub hash: ebe9bcbd185e1973b5447beb306bd9d93051f415 */ + * Stub hash: 4705229124ee243538e712f4af1d94ddc4c3be0b */ ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_dom_import_simplexml, 0, 1, DOMElement, 0) ZEND_ARG_TYPE_INFO(0, node, IS_OBJECT, 0) @@ -114,6 +114,10 @@ ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_DOMNode_getRootNode, 0, 0, ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, options, IS_ARRAY, 1, "null") ZEND_END_ARG_INFO() +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_DOMNode_compareDocumentPosition, 0, 1, IS_LONG, 0) + ZEND_ARG_OBJ_INFO(0, other, DOMNode, 0) +ZEND_END_ARG_INFO() + ZEND_BEGIN_ARG_WITH_TENTATIVE_RETURN_TYPE_INFO_EX(arginfo_class_DOMImplementation_getFeature, 0, 2, IS_NEVER, 0) ZEND_ARG_TYPE_INFO(0, feature, IS_STRING, 0) ZEND_ARG_TYPE_INFO(0, version, IS_STRING, 0) @@ -553,6 +557,7 @@ ZEND_METHOD(DOMNode, removeChild); ZEND_METHOD(DOMNode, replaceChild); ZEND_METHOD(DOMNode, contains); ZEND_METHOD(DOMNode, getRootNode); +ZEND_METHOD(DOMNode, compareDocumentPosition); ZEND_METHOD(DOMImplementation, getFeature); ZEND_METHOD(DOMImplementation, hasFeature); ZEND_METHOD(DOMImplementation, createDocumentType); @@ -745,6 +750,7 @@ static const zend_function_entry class_DOMNode_methods[] = { ZEND_ME(DOMNode, replaceChild, arginfo_class_DOMNode_replaceChild, ZEND_ACC_PUBLIC) ZEND_ME(DOMNode, contains, arginfo_class_DOMNode_contains, ZEND_ACC_PUBLIC) ZEND_ME(DOMNode, getRootNode, arginfo_class_DOMNode_getRootNode, ZEND_ACC_PUBLIC) + ZEND_ME(DOMNode, compareDocumentPosition, arginfo_class_DOMNode_compareDocumentPosition, ZEND_ACC_PUBLIC) ZEND_FE_END }; @@ -1098,6 +1104,42 @@ static zend_class_entry *register_class_DOMNode(void) class_entry = zend_register_internal_class_ex(&ce, NULL); class_entry->ce_flags |= ZEND_ACC_NOT_SERIALIZABLE; + zval const_DOCUMENT_POSITION_DISCONNECTED_value; + ZVAL_LONG(&const_DOCUMENT_POSITION_DISCONNECTED_value, 0x1); + zend_string *const_DOCUMENT_POSITION_DISCONNECTED_name = zend_string_init_interned("DOCUMENT_POSITION_DISCONNECTED", sizeof("DOCUMENT_POSITION_DISCONNECTED") - 1, 1); + zend_declare_typed_class_constant(class_entry, const_DOCUMENT_POSITION_DISCONNECTED_name, &const_DOCUMENT_POSITION_DISCONNECTED_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_LONG)); + zend_string_release(const_DOCUMENT_POSITION_DISCONNECTED_name); + + zval const_DOCUMENT_POSITION_PRECEDING_value; + ZVAL_LONG(&const_DOCUMENT_POSITION_PRECEDING_value, 0x2); + zend_string *const_DOCUMENT_POSITION_PRECEDING_name = zend_string_init_interned("DOCUMENT_POSITION_PRECEDING", sizeof("DOCUMENT_POSITION_PRECEDING") - 1, 1); + zend_declare_typed_class_constant(class_entry, const_DOCUMENT_POSITION_PRECEDING_name, &const_DOCUMENT_POSITION_PRECEDING_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_LONG)); + zend_string_release(const_DOCUMENT_POSITION_PRECEDING_name); + + zval const_DOCUMENT_POSITION_FOLLOWING_value; + ZVAL_LONG(&const_DOCUMENT_POSITION_FOLLOWING_value, 0x4); + zend_string *const_DOCUMENT_POSITION_FOLLOWING_name = zend_string_init_interned("DOCUMENT_POSITION_FOLLOWING", sizeof("DOCUMENT_POSITION_FOLLOWING") - 1, 1); + zend_declare_typed_class_constant(class_entry, const_DOCUMENT_POSITION_FOLLOWING_name, &const_DOCUMENT_POSITION_FOLLOWING_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_LONG)); + zend_string_release(const_DOCUMENT_POSITION_FOLLOWING_name); + + zval const_DOCUMENT_POSITION_CONTAINS_value; + ZVAL_LONG(&const_DOCUMENT_POSITION_CONTAINS_value, 0x8); + zend_string *const_DOCUMENT_POSITION_CONTAINS_name = zend_string_init_interned("DOCUMENT_POSITION_CONTAINS", sizeof("DOCUMENT_POSITION_CONTAINS") - 1, 1); + zend_declare_typed_class_constant(class_entry, const_DOCUMENT_POSITION_CONTAINS_name, &const_DOCUMENT_POSITION_CONTAINS_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_LONG)); + zend_string_release(const_DOCUMENT_POSITION_CONTAINS_name); + + zval const_DOCUMENT_POSITION_CONTAINED_BY_value; + ZVAL_LONG(&const_DOCUMENT_POSITION_CONTAINED_BY_value, 0x10); + zend_string *const_DOCUMENT_POSITION_CONTAINED_BY_name = zend_string_init_interned("DOCUMENT_POSITION_CONTAINED_BY", sizeof("DOCUMENT_POSITION_CONTAINED_BY") - 1, 1); + zend_declare_typed_class_constant(class_entry, const_DOCUMENT_POSITION_CONTAINED_BY_name, &const_DOCUMENT_POSITION_CONTAINED_BY_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_LONG)); + zend_string_release(const_DOCUMENT_POSITION_CONTAINED_BY_name); + + zval const_DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC_value; + ZVAL_LONG(&const_DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC_value, 0x20); + zend_string *const_DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC_name = zend_string_init_interned("DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC", sizeof("DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC") - 1, 1); + zend_declare_typed_class_constant(class_entry, const_DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC_name, &const_DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_LONG)); + zend_string_release(const_DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC_name); + zval property_nodeName_default_value; ZVAL_UNDEF(&property_nodeName_default_value); zend_string *property_nodeName_name = zend_string_init("nodeName", sizeof("nodeName") - 1, 1); diff --git a/ext/dom/tests/compareDocumentPosition/attribute_child_order.phpt b/ext/dom/tests/compareDocumentPosition/attribute_child_order.phpt new file mode 100644 index 0000000000000..84137dcbaa347 --- /dev/null +++ b/ext/dom/tests/compareDocumentPosition/attribute_child_order.phpt @@ -0,0 +1,53 @@ +--TEST-- +compareDocumentPosition: attribute vs child order +--EXTENSIONS-- +dom +--FILE-- +loadXML(<< + + + +

foo

+
+
+

bar

+
+
+
+
+XML); + +$before = $dom->documentElement->firstElementChild; +$outer = $before->nextElementSibling; +$foo = $outer->firstElementChild; +$bar = $foo->nextElementSibling->firstElementChild->firstElementChild; + +// See note about attributes vs children positions: attributes precede children + +echo "--- outer attribute vs before ---\n"; +var_dump($outer->attributes[0]->compareDocumentPosition($before) === DOMNode::DOCUMENT_POSITION_PRECEDING); +var_dump($before->compareDocumentPosition($outer->attributes[0]) === DOMNode::DOCUMENT_POSITION_FOLLOWING); + +echo "--- outer attribute vs foo ---\n"; +var_dump($outer->attributes[0]->compareDocumentPosition($foo) === DOMNode::DOCUMENT_POSITION_FOLLOWING); +var_dump($foo->compareDocumentPosition($outer->attributes[0]) === DOMNode::DOCUMENT_POSITION_PRECEDING); + +echo "--- outer attribute vs bar ---\n"; +var_dump($outer->attributes[0]->compareDocumentPosition($bar) === DOMNode::DOCUMENT_POSITION_FOLLOWING); +var_dump($bar->compareDocumentPosition($outer->attributes[0]) === DOMNode::DOCUMENT_POSITION_PRECEDING); + +?> +--EXPECT-- +--- outer attribute vs before --- +bool(true) +bool(true) +--- outer attribute vs foo --- +bool(true) +bool(true) +--- outer attribute vs bar --- +bool(true) +bool(true) diff --git a/ext/dom/tests/compareDocumentPosition/attribute_order_different_element.phpt b/ext/dom/tests/compareDocumentPosition/attribute_order_different_element.phpt new file mode 100644 index 0000000000000..fed5f9c316b62 --- /dev/null +++ b/ext/dom/tests/compareDocumentPosition/attribute_order_different_element.phpt @@ -0,0 +1,54 @@ +--TEST-- +compareDocumentPosition: attribute order for different element +--EXTENSIONS-- +dom +--FILE-- +loadXML(<< + + + + +XML); + +$attrs1 = $dom->documentElement->firstElementChild->attributes; +$attrs2 = $dom->documentElement->firstElementChild->nextElementSibling->attributes; + +foreach ($attrs1 as $attr1) { + foreach ($attrs2 as $attr2) { + echo "--- Compare 1->2 {$attr1->name} and {$attr2->name} ---\n"; + var_dump($attr1->compareDocumentPosition($attr2) === DOMNode::DOCUMENT_POSITION_FOLLOWING); + echo "--- Compare 2->1 {$attr1->name} and {$attr2->name} ---\n"; + var_dump($attr2->compareDocumentPosition($attr1) === DOMNode::DOCUMENT_POSITION_PRECEDING); + } +} + +?> +--EXPECT-- +--- Compare 1->2 a and a --- +bool(true) +--- Compare 2->1 a and a --- +bool(true) +--- Compare 1->2 a and x --- +bool(true) +--- Compare 2->1 a and x --- +bool(true) +--- Compare 1->2 c and a --- +bool(true) +--- Compare 2->1 c and a --- +bool(true) +--- Compare 1->2 c and x --- +bool(true) +--- Compare 2->1 c and x --- +bool(true) +--- Compare 1->2 e and a --- +bool(true) +--- Compare 2->1 e and a --- +bool(true) +--- Compare 1->2 e and x --- +bool(true) +--- Compare 2->1 e and x --- +bool(true) diff --git a/ext/dom/tests/compareDocumentPosition/attribute_order_same_element.phpt b/ext/dom/tests/compareDocumentPosition/attribute_order_same_element.phpt new file mode 100644 index 0000000000000..1987d179ea0e8 --- /dev/null +++ b/ext/dom/tests/compareDocumentPosition/attribute_order_same_element.phpt @@ -0,0 +1,40 @@ +--TEST-- +compareDocumentPosition: attribute order for same element +--EXTENSIONS-- +dom +--FILE-- +loadXML(<< + +XML); + +$attrs = $dom->documentElement->attributes; +for ($i = 0; $i <= 2; $i++) { + for ($j = $i + 1; $j <= $i + 2; $j++) { + echo "--- Compare $i and ", ($j % 3), " ---\n"; + if ($i < ($j % 3)) { + $expected = DOMNode::DOCUMENT_POSITION_FOLLOWING | DOMNode::DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC; + } else { + $expected = DOMNode::DOCUMENT_POSITION_PRECEDING | DOMNode::DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC; + } + var_dump($attrs[$i]->compareDocumentPosition($attrs[$j % 3]) === $expected); + } +} + +?> +--EXPECT-- +--- Compare 0 and 1 --- +bool(true) +--- Compare 0 and 2 --- +bool(true) +--- Compare 1 and 2 --- +bool(true) +--- Compare 1 and 0 --- +bool(true) +--- Compare 2 and 0 --- +bool(true) +--- Compare 2 and 1 --- +bool(true) diff --git a/ext/dom/tests/compareDocumentPosition/contains_attribute_direct_descendent.phpt b/ext/dom/tests/compareDocumentPosition/contains_attribute_direct_descendent.phpt new file mode 100644 index 0000000000000..0cf5785988bb9 --- /dev/null +++ b/ext/dom/tests/compareDocumentPosition/contains_attribute_direct_descendent.phpt @@ -0,0 +1,23 @@ +--TEST-- +compareDocumentPosition: contains attribute as a direct descendent +--EXTENSIONS-- +dom +--FILE-- +loadXML(<< + +XML); + +$container = $dom->documentElement; +$attribute = $container->attributes[0]; + +var_dump($container->compareDocumentPosition($attribute) === (DOMNode::DOCUMENT_POSITION_FOLLOWING | DOMNode::DOCUMENT_POSITION_CONTAINED_BY)); +var_dump($attribute->compareDocumentPosition($container) === (DOMNode::DOCUMENT_POSITION_PRECEDING | DOMNode::DOCUMENT_POSITION_CONTAINS)); + +?> +--EXPECT-- +bool(true) +bool(true) diff --git a/ext/dom/tests/compareDocumentPosition/contains_attribute_freestanding.phpt b/ext/dom/tests/compareDocumentPosition/contains_attribute_freestanding.phpt new file mode 100644 index 0000000000000..d13208cddda03 --- /dev/null +++ b/ext/dom/tests/compareDocumentPosition/contains_attribute_freestanding.phpt @@ -0,0 +1,22 @@ +--TEST-- +compareDocumentPosition: contains attribute for a freestanding element +--EXTENSIONS-- +dom +--FILE-- +createElement("foo"); +$foo->setAttribute("test", "value"); +$attribute = $foo->attributes[0]; + +echo $dom->saveXML($foo), "\n"; + +var_dump($foo->compareDocumentPosition($attribute) === (DOMNode::DOCUMENT_POSITION_FOLLOWING | DOMNode::DOCUMENT_POSITION_CONTAINED_BY)); +var_dump($attribute->compareDocumentPosition($foo) === (DOMNode::DOCUMENT_POSITION_PRECEDING | DOMNode::DOCUMENT_POSITION_CONTAINS)); + +?> +--EXPECT-- + +bool(true) +bool(true) diff --git a/ext/dom/tests/compareDocumentPosition/contains_attribute_longer_descendent.phpt b/ext/dom/tests/compareDocumentPosition/contains_attribute_longer_descendent.phpt new file mode 100644 index 0000000000000..f95a35edc78f7 --- /dev/null +++ b/ext/dom/tests/compareDocumentPosition/contains_attribute_longer_descendent.phpt @@ -0,0 +1,28 @@ +--TEST-- +compareDocumentPosition: contains attribute as a descendent in a longer path +--EXTENSIONS-- +dom +--FILE-- +loadXML(<< + +
+

+

+
+XML); + +$container = $dom->documentElement; +$p = $container->firstElementChild->firstElementChild; +$attribute = $p->attributes[0]; + +var_dump($container->compareDocumentPosition($attribute) === (DOMNode::DOCUMENT_POSITION_FOLLOWING | DOMNode::DOCUMENT_POSITION_CONTAINED_BY)); +var_dump($attribute->compareDocumentPosition($container) === (DOMNode::DOCUMENT_POSITION_PRECEDING | DOMNode::DOCUMENT_POSITION_CONTAINS)); + +?> +--EXPECT-- +bool(true) +bool(true) diff --git a/ext/dom/tests/compareDocumentPosition/contains_element_direct_descendent.phpt b/ext/dom/tests/compareDocumentPosition/contains_element_direct_descendent.phpt new file mode 100644 index 0000000000000..5e00c8ec49719 --- /dev/null +++ b/ext/dom/tests/compareDocumentPosition/contains_element_direct_descendent.phpt @@ -0,0 +1,25 @@ +--TEST-- +compareDocumentPosition: contains nodes as a direct descendent +--EXTENSIONS-- +dom +--FILE-- +loadXML(<< + +
+ +XML); + +$container = $dom->documentElement; +$div = $container->firstChild; + +var_dump($container->compareDocumentPosition($div) === (DOMNode::DOCUMENT_POSITION_FOLLOWING | DOMNode::DOCUMENT_POSITION_CONTAINED_BY)); +var_dump($div->compareDocumentPosition($container) === (DOMNode::DOCUMENT_POSITION_PRECEDING | DOMNode::DOCUMENT_POSITION_CONTAINS)); + +?> +--EXPECT-- +bool(true) +bool(true) diff --git a/ext/dom/tests/compareDocumentPosition/contains_element_longer_descendent.phpt b/ext/dom/tests/compareDocumentPosition/contains_element_longer_descendent.phpt new file mode 100644 index 0000000000000..8557b5407e0f4 --- /dev/null +++ b/ext/dom/tests/compareDocumentPosition/contains_element_longer_descendent.phpt @@ -0,0 +1,44 @@ +--TEST-- +compareDocumentPosition: contains nodes as a descendent in a longer path +--EXTENSIONS-- +dom +--FILE-- +loadXML(<< + + + + + + + + +XML); + +$xpath = new DOMXPath($dom); +$container = $xpath->query("//container")->item(0); + +foreach (["a", "b", "c", "d"] as $tag) { + echo "--- Test $tag ---\n"; + $element = $xpath->query("//$tag")->item(0); + var_dump($container->compareDocumentPosition($element) === (DOMNode::DOCUMENT_POSITION_FOLLOWING | DOMNode::DOCUMENT_POSITION_CONTAINED_BY)); + var_dump($element->compareDocumentPosition($container) === (DOMNode::DOCUMENT_POSITION_PRECEDING | DOMNode::DOCUMENT_POSITION_CONTAINS)); +} + +?> +--EXPECT-- +--- Test a --- +bool(true) +bool(true) +--- Test b --- +bool(true) +bool(true) +--- Test c --- +bool(true) +bool(true) +--- Test d --- +bool(true) +bool(true) diff --git a/ext/dom/tests/compareDocumentPosition/disconnected.phpt b/ext/dom/tests/compareDocumentPosition/disconnected.phpt new file mode 100644 index 0000000000000..75b262b9848e1 --- /dev/null +++ b/ext/dom/tests/compareDocumentPosition/disconnected.phpt @@ -0,0 +1,68 @@ +--TEST-- +compareDocumentPosition: disconnected +--EXTENSIONS-- +dom +--FILE-- +2): "; + var_dump(($element1->compareDocumentPosition($element2) & (DOMNode::DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC | DOMNode::DOCUMENT_POSITION_DISCONNECTED)) === (DOMNode::DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC | DOMNode::DOCUMENT_POSITION_DISCONNECTED)); + echo "Disconnect and implementation flag (2->1): "; + var_dump(($element2->compareDocumentPosition($element1) & (DOMNode::DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC | DOMNode::DOCUMENT_POSITION_DISCONNECTED)) === (DOMNode::DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC | DOMNode::DOCUMENT_POSITION_DISCONNECTED)); + + // Must be the opposite result + echo "Opposite result: "; + if ($element1->compareDocumentPosition($element2) & DOMNode::DOCUMENT_POSITION_FOLLOWING) { + var_dump(($element2->compareDocumentPosition($element1) & DOMNode::DOCUMENT_POSITION_PRECEDING) === DOMNode::DOCUMENT_POSITION_PRECEDING); + } else { + var_dump(($element2->compareDocumentPosition($element1) & DOMNode::DOCUMENT_POSITION_FOLLOWING) === DOMNode::DOCUMENT_POSITION_FOLLOWING); + } +} + +$dom1 = new DOMDocument(); +$dom1->loadXML(''); +$dom2 = new DOMDocument(); +$dom2->loadXML(''); + +echo "--- Two documents ---\n"; +check($dom1, $dom2); +echo "--- Two document roots ---\n"; +check($dom1->documentElement, $dom2->documentElement); +echo "--- Fragment ---\n"; +$fragment = $dom1->createDocumentFragment(); +$foo = $fragment->appendChild(new DOMText("foo")); +check($foo, $dom1); +echo "--- Unattached element ---\n"; +check(new DOMElement("foo"), new DOMElement("bar")); +echo "--- Unattached attribute ---\n"; +check(new DOMAttr("foo"), new DOMAttr("bar")); +echo "--- Unattached attribute & element ---\n"; +check(new DOMAttr("foo"), new DOMElement("bar")); + +?> +--EXPECT-- +--- Two documents --- +Disconnect and implementation flag (1->2): bool(true) +Disconnect and implementation flag (2->1): bool(true) +Opposite result: bool(true) +--- Two document roots --- +Disconnect and implementation flag (1->2): bool(true) +Disconnect and implementation flag (2->1): bool(true) +Opposite result: bool(true) +--- Fragment --- +Disconnect and implementation flag (1->2): bool(true) +Disconnect and implementation flag (2->1): bool(true) +Opposite result: bool(true) +--- Unattached element --- +Disconnect and implementation flag (1->2): bool(true) +Disconnect and implementation flag (2->1): bool(true) +Opposite result: bool(true) +--- Unattached attribute --- +Disconnect and implementation flag (1->2): bool(true) +Disconnect and implementation flag (2->1): bool(true) +Opposite result: bool(true) +--- Unattached attribute & element --- +Disconnect and implementation flag (1->2): bool(true) +Disconnect and implementation flag (2->1): bool(true) +Opposite result: bool(true) diff --git a/ext/dom/tests/compareDocumentPosition/element_order_different_depth.phpt b/ext/dom/tests/compareDocumentPosition/element_order_different_depth.phpt new file mode 100644 index 0000000000000..cca1174961db9 --- /dev/null +++ b/ext/dom/tests/compareDocumentPosition/element_order_different_depth.phpt @@ -0,0 +1,50 @@ +--TEST-- +compareDocumentPosition: element order at a different tree depth +--EXTENSIONS-- +dom +--FILE-- +loadXML(<< + +
+

foo

+
+
+
+

bar

+
+
+
+XML); + +$xpath = new DOMXPath($dom); +$query = $xpath->query("//p"); +$foo = $query->item(0); +$bar = $query->item(1); + +for ($i = 0; $i < 2; $i++) { + echo "--- Check on depth $i ---\n"; + var_dump($foo->compareDocumentPosition($bar) === DOMNode::DOCUMENT_POSITION_FOLLOWING); + var_dump($bar->compareDocumentPosition($foo) === DOMNode::DOCUMENT_POSITION_PRECEDING); + $foo = $foo->parentElement; + $bar = $bar->parentElement; +} + +echo "--- One contains the other at depth 2 ---\n"; +var_dump(boolval($foo->compareDocumentPosition($bar) & DOMNode::DOCUMENT_POSITION_CONTAINED_BY)); +var_dump(boolval($bar->compareDocumentPosition($foo) & DOMNode::DOCUMENT_POSITION_CONTAINS)); + +?> +--EXPECT-- +--- Check on depth 0 --- +bool(true) +bool(true) +--- Check on depth 1 --- +bool(true) +bool(true) +--- One contains the other at depth 2 --- +bool(true) +bool(true) diff --git a/ext/dom/tests/compareDocumentPosition/element_order_direct_root_children.phpt b/ext/dom/tests/compareDocumentPosition/element_order_direct_root_children.phpt new file mode 100644 index 0000000000000..b45f2435d7090 --- /dev/null +++ b/ext/dom/tests/compareDocumentPosition/element_order_direct_root_children.phpt @@ -0,0 +1,18 @@ +--TEST-- +compareDocumentPosition: direct root children +--EXTENSIONS-- +dom +--FILE-- +appendChild($dom->createElement("foo")); +$bar = $dom->appendChild($dom->createElement("bar")); + +var_dump($foo->compareDocumentPosition($bar) === DOMNode::DOCUMENT_POSITION_FOLLOWING); +var_dump($bar->compareDocumentPosition($foo) === DOMNode::DOCUMENT_POSITION_PRECEDING); + +?> +--EXPECT-- +bool(true) +bool(true) diff --git a/ext/dom/tests/compareDocumentPosition/element_order_same_depth.phpt b/ext/dom/tests/compareDocumentPosition/element_order_same_depth.phpt new file mode 100644 index 0000000000000..e079b1c8d5566 --- /dev/null +++ b/ext/dom/tests/compareDocumentPosition/element_order_same_depth.phpt @@ -0,0 +1,71 @@ +--TEST-- +compareDocumentPosition: element order at the same tree depth +--EXTENSIONS-- +dom +--FILE-- +loadXML(<< + +
+
+ +

foo

+
+
+
+
+

bar

+ +
+
+
+XML); + +$xpath = new DOMXPath($dom); +$query = $xpath->query("//p"); +$foo = $query->item(0); +$bar = $query->item(1); + +echo "--- strong & foo ---\n"; +var_dump($foo->previousElementSibling->compareDocumentPosition($foo) === DOMNode::DOCUMENT_POSITION_FOLLOWING); +var_dump($foo->compareDocumentPosition($foo->previousElementSibling) === DOMNode::DOCUMENT_POSITION_PRECEDING); + +echo "--- strong & bar ---\n"; +var_dump($bar->nextElementSibling->compareDocumentPosition($bar) === DOMNode::DOCUMENT_POSITION_PRECEDING); +var_dump($bar->compareDocumentPosition($bar->nextElementSibling) === DOMNode::DOCUMENT_POSITION_FOLLOWING); + +echo "--- strong & strong ---\n"; +var_dump($foo->previousElementSibling->compareDocumentPosition($bar->nextElementSibling) === DOMNode::DOCUMENT_POSITION_FOLLOWING); +var_dump($bar->nextElementSibling->compareDocumentPosition($foo->previousElementSibling) === DOMNode::DOCUMENT_POSITION_PRECEDING); + +for ($i = 0; $i < 3; $i++) { + echo "--- Check on depth $i ---\n"; + var_dump($foo->compareDocumentPosition($bar) === DOMNode::DOCUMENT_POSITION_FOLLOWING); + var_dump($bar->compareDocumentPosition($foo) === DOMNode::DOCUMENT_POSITION_PRECEDING); + $foo = $foo->parentElement; + $bar = $bar->parentElement; +} + +?> +--EXPECT-- +--- strong & foo --- +bool(true) +bool(true) +--- strong & bar --- +bool(true) +bool(true) +--- strong & strong --- +bool(true) +bool(true) +--- Check on depth 0 --- +bool(true) +bool(true) +--- Check on depth 1 --- +bool(true) +bool(true) +--- Check on depth 2 --- +bool(true) +bool(true) diff --git a/ext/dom/tests/compareDocumentPosition/entity.phpt b/ext/dom/tests/compareDocumentPosition/entity.phpt new file mode 100644 index 0000000000000..318053c454f9b --- /dev/null +++ b/ext/dom/tests/compareDocumentPosition/entity.phpt @@ -0,0 +1,51 @@ +--TEST-- +compareDocumentPosition: entity ordering +--EXTENSIONS-- +dom +--FILE-- +loadXML(<< + + +]> + + &e1; + +XML, LIBXML_NOENT); + +$entities = iterator_to_array($dom->doctype->entities); +usort($entities, fn ($a, $b) => strcmp($a->nodeName, $b->nodeName)); + +echo "--- Compare entities ---\n"; + +var_dump($entities[0]->compareDocumentPosition($entities[1]) === DOMNode::DOCUMENT_POSITION_FOLLOWING); +var_dump($entities[1]->compareDocumentPosition($entities[0]) === DOMNode::DOCUMENT_POSITION_PRECEDING); + +$xpath = new DOMXPath($dom); +$child = $xpath->query('//child')->item(0); + +echo "--- Compare entities against first child ---\n"; + +var_dump($entities[0]->compareDocumentPosition($child->firstChild) === DOMNode::DOCUMENT_POSITION_FOLLOWING); +var_dump($entities[1]->compareDocumentPosition($child->firstChild) === DOMNode::DOCUMENT_POSITION_FOLLOWING); + +echo "--- Compare first child against entities ---\n"; + +var_dump($child->firstChild->compareDocumentPosition($entities[0]) === DOMNode::DOCUMENT_POSITION_PRECEDING); +var_dump($child->firstChild->compareDocumentPosition($entities[1]) === DOMNode::DOCUMENT_POSITION_PRECEDING); + +?> +--EXPECT-- +--- Compare entities --- +bool(true) +bool(true) +--- Compare entities against first child --- +bool(true) +bool(true) +--- Compare first child against entities --- +bool(true) +bool(true) diff --git a/ext/dom/tests/compareDocumentPosition/equal.phpt b/ext/dom/tests/compareDocumentPosition/equal.phpt new file mode 100644 index 0000000000000..31f30d542b406 --- /dev/null +++ b/ext/dom/tests/compareDocumentPosition/equal.phpt @@ -0,0 +1,29 @@ +--TEST-- +compareDocumentPosition: equal nodes +--EXTENSIONS-- +dom +--FILE-- +loadXML(<< + +

foo

+

foo

+
+XML); + +$xpath = new DOMXPath($dom); +$nodes = $xpath->query('//p'); +var_dump($nodes->item(0)->compareDocumentPosition($nodes->item(0)) === 0); +var_dump($nodes->item(1)->compareDocumentPosition($nodes->item(1)) === 0); +var_dump($nodes->item(0)->compareDocumentPosition($nodes->item(1)) === 0); +var_dump($nodes->item(1)->compareDocumentPosition($nodes->item(0)) === 0); + +?> +--EXPECT-- +bool(true) +bool(true) +bool(false) +bool(false)