Skip to content
This repository has been archived by the owner on Mar 19, 2020. It is now read-only.

Commit

Permalink
Added DOM wrap() and more tests
Browse files Browse the repository at this point in the history
  • Loading branch information
TonyBogdanov committed Apr 1, 2018
1 parent 876a3d0 commit 5af1dcc
Show file tree
Hide file tree
Showing 44 changed files with 3,597 additions and 2,399 deletions.
114 changes: 111 additions & 3 deletions classes/Dom.php
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ protected static function createInvalidContentException($content): \InvalidArgum
*
* Add all nodes that match at least one of the selector tokens to the specified Dom collection and return it.
*
* The supplied $effectiveRoot node will be considered as the root of the tree, even if there are more ancestors.
* The supplied $effectiveRoot node will be considered the root of the tree, even if there are more ancestors.
* Child nodes will be treated, when matching, as if they don't have a parent node.
*
* @param Dom $dom
Expand Down Expand Up @@ -121,6 +121,47 @@ protected static function traverseMatch(
return $dom;
}

/**
* Loop over the supplied array of nodes and return the first matched element node or NULL.
*
* @param NodeInterface[] $nodes
* @return null|Element
*/
protected static function findFirstElement(array $nodes): ?Element
{
foreach ($nodes as $node) {
if ($node instanceof Element) {
return $node;
}
}
return null;
}

/**
* Loop over the supplied array of nodes and find the first matched element node. If it has child nodes, find the
* first matched element child, then recurse until the inner-most element node with no children (element nodes)
* is found and return it.
*
* If the array does not contain an element node, return NULL.
*
* @param NodeInterface[] $nodes
* @param null|Element $fallback
* @return null|Element
*/
protected static function findFirstInnermostElement(array $nodes, Element $fallback = null): ?Element
{
$element = static::findFirstElement($nodes);
if (!$element) {
return $fallback;
}

if ($element->isVoid() || 0 === count($element)) {
return $element;
}

return static::findFirstInnermostElement($element->getIterator()->getArrayCopy(), $element);
}

/**
* Dom constructor.
* Create new collection from the specified content.
Expand Down Expand Up @@ -396,14 +437,16 @@ public function children(): Dom
*/
public function append($content): Dom
{
$nodes = (new static($content))->nodes;

/** @var NodeInterface $node */
foreach ($this->nodes as $node) {
if (!$node instanceof Element) {
continue;
}

/** @var NodeInterface $child */
foreach ((new static($content))->nodes as $child) {
foreach ($nodes as $child) {
$node->insertAfter(null === $child->parent() ? $child : $child->clone());
}
}
Expand All @@ -429,14 +472,16 @@ public function append($content): Dom
*/
public function prepend($content): Dom
{
$nodes = array_reverse((new static($content))->nodes);

/** @var NodeInterface $node */
foreach ($this->nodes as $node) {
if (!$node instanceof Element) {
continue;
}

/** @var NodeInterface $child */
foreach (array_reverse((new static($content))->nodes) as $child) {
foreach ($nodes as $child) {
$node->insertBefore(null === $child->parent() ? $child : $child->clone());
}
}
Expand Down Expand Up @@ -516,6 +561,69 @@ public function after($content): Dom
return $this;
}

/**
* Wrap a clone of the supplied content around each node with a parent (not only element nodes) in the collection.
*
* If the content resolves to a collection of more than one wrapping element node, use only the first one.
* If the wrapping element has children use a single-element-per-level sub-tree, where the wrapping element is
* the root and may contain one and only one element node, which may contain another one and only one element node
* and so on.
*
* Wrap the current collection nodes in clones of this sub-tree as immediate children of the inner-most element
* node of the tree. If a node in the collection does not have a parent element, ignore it.
*
* If the wrapping collection does not contain at least one element node, do nothing.
*
* Return the original collection for chaining.
*
* @param $content
* @return Dom
*/
public function wrap($content): Dom
{
// get a detached clone of the first element node in the content
/** @var Element $wrapper */
$wrapper = static::findFirstElement((new static($content))->nodes);
if (!$wrapper) {
return $this;
}
$wrapper = $wrapper->clone()->detach();

// collapse the wrapper down to a single-element-per-level tree of clones
$inner = $wrapper;
while (0 < count($inner)) {
$element = static::findFirstElement($inner->getIterator()->getArrayCopy());
if (!$element) {
break;
}
$inner->clear()->insertAfter($inner = $element->clone());
}

// wrap nodes
foreach ($this->nodes as $node) {
if (null === $node->parent()) {
continue;
}

/** @var Element $insert */
$insert = $wrapper->clone();

/** @var Element $parent */
$parent = $node->parent();
$parent->insertAfter($insert, $node);

$inner = static::findFirstInnermostElement($insert->getIterator()->getArrayCopy());

if ($inner) {
$inner->insertAfter($node);
} else {
$insert->insertAfter($node);
}
}

return $this;
}

/**
* Return a new Dom collection of all the descendants of each Element node in the current collection,
* filtered by the specified CSS selector.
Expand Down
2 changes: 2 additions & 0 deletions classes/Node/Element.php
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,7 @@ public function isChild(NodeInterface $node): bool
*
* @param int $index
* @return NodeInterface
* @throws \OutOfBoundsException
*/
public function get(int $index): NodeInterface
{
Expand All @@ -349,6 +350,7 @@ public function get(int $index): NodeInterface
*
* @param NodeInterface $node
* @return int
* @throws \InvalidArgumentException
*/
public function index(NodeInterface $node): int
{
Expand Down
2 changes: 1 addition & 1 deletion classes/Node/NodeInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ public function parent(): ?NodeInterface;
public function attach(NodeInterface $parent): NodeInterface;

/**
* Detach this node from it's parent, if such is set by both removing the parent node reference, and removing this
* Detach this node from it's parent, if such is set, by both removing the parent node reference, and removing this
* node as a child node from the parent when applicable.
*
* @return $this
Expand Down
2 changes: 1 addition & 1 deletion classes/SelectorMatcher.php
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ public function match(Css\NodeInterface $token, Dom\Element $node, Dom\Element $
return $this->match($token->getTree(), $node, $effectiveRoot);

case $token instanceof Css\ElementNode:
return $this->matchElementNode($token, $node, $effectiveRoot);
return $this->matchElementNode($token, $node);

case $token instanceof Css\AttributeNode:
return $this->matchAttributeNode($token, $node, $effectiveRoot);
Expand Down
81 changes: 44 additions & 37 deletions classes/SelectorMatcher/CombinedSelectorNodeTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,14 @@ protected function matchDescendantCombinedSelectorNode(
return false;
}

// node must match the sub-selector
if (!$this->match($token->getSubSelector(), $node, $effectiveRoot)) {
return false;
}

$parent = $node;

// node must have a parent that matches the selector, anywhere up the chain
do {
/** @var Element $parent */
$parent = $parent->parent();
Expand Down Expand Up @@ -76,6 +82,11 @@ protected function matchChildCombinedSelectorNode(
/** @var Element $parent */
$parent = $node->parent();

// node must match the sub-selector
if (!$this->match($token->getSubSelector(), $node, $effectiveRoot)) {
return false;
}

// node's parent must match the selector
return $this->match($token->getSelector(), $parent, $effectiveRoot);
}
Expand All @@ -100,24 +111,22 @@ protected function matchAdjacentCombinedSelectorNode(
$parent = $node->parent();

// node must have an immediately preceding sibling that matches the selector
try {
// don't bother if the node is the first child (no siblings on the left)
$index = $parent->index($node);
if (0 === $index) {
return false;
}

// don't bother if the sibling is not an Element node
$sibling = $parent->get($index - 1);
if (!$sibling instanceof Element) {
return false;
}
// don't bother if the node is the first child (no siblings on the left)
// ignored \InvalidArgumentException as $node is always a child of $parent
$index = $parent->index($node);
if (0 === $index) {
return false;
}

// match the selector
return $this->match($token->getSelector(), $sibling, $effectiveRoot);
} catch (\OutOfBoundsException|\InvalidArgumentException $e) {
// don't bother if the sibling is not an Element node
// ignored \OutOfBoundsException as $index will always be within the list of children
$sibling = $parent->get($index - 1);
if (!$sibling instanceof Element) {
return false;
}

// match the selector
return $this->match($token->getSelector(), $sibling, $effectiveRoot);
}

/**
Expand All @@ -140,32 +149,30 @@ protected function matchGeneralSiblingCombinedSelectorNode(
$parent = $node->parent();

// node must have a preceding sibling (may not be immediate) that matches the selector
try {
// don't bother if the node is the first child (no siblings on the left)
$index = $parent->index($node);
if (0 === $index) {
return false;
}
// don't bother if the node is the first child (no siblings on the left)
// ignored \InvalidArgumentException as $node is always a child of $parent
$index = $parent->index($node);
if (0 === $index) {
return false;
}

// test all preceding siblings & bail after the first successful match
for ($i = $index - 1; $i >= 0; $i--) {
// skip the sibling if it's not an Element node
$sibling = $parent->get($i);
if (!$sibling instanceof Element) {
continue;
}

// match the selector
if ($this->match($token->getSelector(), $sibling, $effectiveRoot)) {
return true;
}
// test all preceding siblings & bail after the first successful match
for ($i = $index - 1; $i >= 0; $i--) {
// skip the sibling if it's not an Element node
// ignored \OutOfBoundsException as $index will always be within the list of children
$sibling = $parent->get($i);
if (!$sibling instanceof Element) {
continue;
}

// no sibling matches the selector
return false;
} catch (\OutOfBoundsException|\InvalidArgumentException $e) {
return false;
// match the selector
if ($this->match($token->getSelector(), $sibling, $effectiveRoot)) {
return true;
}
}

// no sibling matches the selector
return false;
}

/**
Expand Down
3 changes: 1 addition & 2 deletions classes/SelectorMatcher/ElementNodeTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,9 @@ trait ElementNodeTrait
/**
* @param ElementNode $token
* @param Element $node
* @param null|Element $effectiveRoot
* @return bool
*/
protected function matchElementNode(ElementNode $token, Element $node, Element $effectiveRoot = null): bool
protected function matchElementNode(ElementNode $token, Element $node): bool
{
// target element tag name may be null, directly return true as ElementNode tokens have no sub-selectors
if (null === $token->getElement()) {
Expand Down
Loading

0 comments on commit 5af1dcc

Please sign in to comment.