From 7da36d94db3594668a7ab7d0032623f63cd773e8 Mon Sep 17 00:00:00 2001 From: gravitypdf-bot Date: Mon, 26 Oct 2026 16:59:06 +1100 Subject: [PATCH 1/3] Copy replaceAll() replacements with their children replaceAll() made a shallow clone of the replacement and a shallow import of that, so each replaced node arrived with its attributes but no children. Import the replacement deeply instead. Fixes #135 Co-Authored-By: Claude Opus 5.5 --- src/Helpers/QueryMutators.php | 3 +-- tests/Issues/Issue135Test.php | 47 +++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 2 deletions(-) create mode 100644 tests/Issues/Issue135Test.php diff --git a/src/Helpers/QueryMutators.php b/src/Helpers/QueryMutators.php index 93ab6b6..ee388f5 100644 --- a/src/Helpers/QueryMutators.php +++ b/src/Helpers/QueryMutators.php @@ -847,8 +847,7 @@ public function replaceAll($selector, DOMDocument $document): Query } foreach ((new DOMTraverser($scope))->find($selector)->matches() as $item) { - $node = $replacement->cloneNode(); - $node = $document->importNode($node); + $node = $document->importNode($replacement, true); $item->parentNode->replaceChild($node, $item); } diff --git a/tests/Issues/Issue135Test.php b/tests/Issues/Issue135Test.php new file mode 100644 index 0000000..87f051c --- /dev/null +++ b/tests/Issues/Issue135Test.php @@ -0,0 +1,47 @@ +

')->get(0)->ownerDocument; + + qp('hi', 'x')->replaceAll('p', $document); + + $copies = qp($document)->children(); + $this->assertSame(['x', 'x'], array_map(function ($node) { + return $node->tagName; + }, $copies->get())); + foreach ($copies->get() as $copy) { + $this->assertSame('1', $copy->getAttribute('a')); + $this->assertSame('hi', $copy->textContent); + $this->assertSame(1, qp($copy)->children('y')->count()); + } + } + + public function testReplaceAllInsertsSeparateCopies() + { + $document = qp('

')->get(0)->ownerDocument; + $source = qp('', 'x'); + + $source->replaceAll('p', $document); + + $copies = qp($document)->find('x')->get(); + $this->assertCount(2, $copies); + $this->assertNotSame($copies[0], $copies[1]); + $this->assertNotSame($copies[0]->firstChild, $copies[1]->firstChild); + $this->assertSame(1, qp($source->get(0)->ownerDocument)->find('x > y')->count()); + } +} From b57043f3dbbbad8ad8e9a744bb582fd5aac10f09 Mon Sep 17 00:00:00 2001 From: gravitypdf-bot Date: Mon, 26 Oct 2026 17:18:23 +1100 Subject: [PATCH 2/3] Add a CHANGELOG entry Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 527a87d..8bcca3a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -58,6 +58,7 @@ QueryPath Changelog - Consolidate the sibling and ancestor walks behind `next()`, `prev()`, `nextAll()`, `prevAll()`, `nextUntil()`, `prevUntil()`, `parent()`, `parents()`, `parentsUntil()` and `closest()` into one helper, so a fix to one no longer has to be repeated in its mirror (issue #115). As a side effect, `parentsUntil()` no longer loops forever on an element that is not attached to the document, and `nextUntil('')` and `prevUntil('')` treat an empty selector as no selector, as the other methods already did, instead of throwing `QueryPath\CSS\ParseException`. `closest('')` returns an empty set, as in jQuery. `nextUntil()`, `prevUntil()` and `parentsUntil()` also stop at an element already collected from an earlier starting element, as `nextAll()` and `prevAll()` do, so `nextUntil()` on each of 2000 siblings falls from about 100 ms to 1 ms - **Behaviour change.** `prevAll()`, `prevUntil()`, `nextAll()`, `nextUntil()`, `siblings()`, `parent()` and `closest()` now return the results for a multi-element set in jQuery's order, instead of grouped by starting element (issue #117). `prevAll()` and `prevUntil()` return reverse document order, as `parents()` and `parentsUntil()` already did, and the others return document order. `qp($xml, 'c')->add('d')->prevAll()` now returns `c, b, a` rather than `b, a, c`. `next()` and `prev()` keep the order of the starting elements, as in jQuery, and the results for a single starting element are unchanged - **Behaviour change.** A positional pseudo-class (`:first`, `:eq()`, `:last`, ...) in the stop selector of `nextUntil()`, `prevUntil()` and `parentsUntil()` now counts across the whole document, as jQuery's does, instead of seeing each sibling or ancestor as a one-element set (issue #118). `nextUntil(':first')` used to stop at once and now walks to the end, and `nextUntil(':eq(3)')` stops at the fourth element in the document. `closest()` no longer matches a positional selector at all, as in jQuery, where `closest(':first')` used to return the element itself. `next()`, `prev()` and `parent()` still test each candidate on its own, which agrees with jQuery for a single starting element +- Fix `replaceAll()` inserting empty copies of the replacement. It copied the replacement node without its children, so only the element and its attributes arrived. It now copies the whole subtree (issue #135) # 4.2.0 From 0d24b17265fe1e2d52c72e58a7ac166011494617 Mon Sep 17 00:00:00 2001 From: gravitypdf-bot Date: Mon, 26 Oct 2026 17:26:25 +1100 Subject: [PATCH 3/3] Tidy comments Co-Authored-By: Claude Opus 5.5 --- tests/Issues/Issue135Test.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Issues/Issue135Test.php b/tests/Issues/Issue135Test.php index 87f051c..c6deeaf 100644 --- a/tests/Issues/Issue135Test.php +++ b/tests/Issues/Issue135Test.php @@ -4,7 +4,7 @@ * * Regression tests for https://github.com/GravityPDF/querypath/issues/135 * - * replaceAll() copied each replacement without its children. + * replaceAll() inserts each replacement with its children. */ namespace QueryPathTests;