diff --git a/README.md b/README.md
index a25e8b4..8e82d92 100644
--- a/README.md
+++ b/README.md
@@ -3,6 +3,8 @@
[![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE)
[![Build Status](https://api.travis-ci.org/xinningsu/html-query.svg?branch=master)](https://travis-ci.org/xinningsu/html-query)
[![Coverage Status](https://coveralls.io/repos/github/xinningsu/html-query/badge.svg?branch=master)](https://coveralls.io/github/xinningsu/html-query)
+[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/xinningsu/html-query/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/xinningsu/html-query)
+[![Code Intelligence Status](https://scrutinizer-ci.com/g/xinningsu/html-query/badges/code-intelligence.svg?b=master)](https://scrutinizer-ci.com/g/xinningsu/html-query)
A jQuery-like html processor written in PHP
diff --git a/src/HQ.php b/src/HQ.php
index 3172d5d..053eeea 100644
--- a/src/HQ.php
+++ b/src/HQ.php
@@ -16,7 +16,7 @@ class HQ
*
* @param string $html
*
- * @return HtmlQuery
+ * @return HtmlDocument
*/
public static function html(string $html)
{
@@ -28,7 +28,7 @@ public static function html(string $html)
*
* @param string $file
*
- * @return HtmlQuery
+ * @return HtmlDocument
*/
public static function htmlFile(string $file)
{
@@ -40,7 +40,7 @@ public static function htmlFile(string $file)
*
* @param string|null $html
*
- * @return HtmlQuery
+ * @return HtmlDocument
*/
public static function instance(?string $html = null)
{
@@ -50,6 +50,6 @@ public static function instance(?string $html = null)
$doc->loadHTML($html, LIBXML_HTML_NODEFDTD | LIBXML_HTML_NOIMPLIED);
}
- return new HtmlQuery($doc, $doc);
+ return new HtmlDocument($doc);
}
}
diff --git a/src/Helper.php b/src/Helper.php
index f4a8478..103e83b 100644
--- a/src/Helper.php
+++ b/src/Helper.php
@@ -2,6 +2,7 @@
namespace Sulao\HtmlQuery;
+use DOMDocument, DOMNode, DOMNodeList, DOMXPath;
use Symfony\Component\CssSelector\CssSelectorConverter;
use Traversable;
@@ -83,6 +84,26 @@ public static function strictArrayDiff(array $arr1, array $arr2): array
return array_values($arr);
}
+ /**
+ * Case insensitive search
+ *
+ * @param string $needle
+ * @param string[] $haystack
+ *
+ * @return array
+ */
+ public static function caseInsensitiveSearch(
+ string $needle,
+ array $haystack
+ ): array {
+ $needle = strtolower($needle);
+ $match = array_filter($haystack, function ($value) use ($needle) {
+ return $needle === strtolower($value);
+ });
+
+ return array_values($match);
+ }
+
/**
* Split the class attr value to a class array
*
@@ -176,4 +197,53 @@ public static function isIdSelector(
return false;
}
+
+ /**
+ * Query xpath to an array of DOMNode
+ *
+ * @param string $xpath
+ * @param DOMDocument $doc
+ * @param DOMNode|null $node
+ *
+ * @return DOMNode[]
+ */
+ public static function xpathQuery(
+ string $xpath,
+ DOMDocument $doc,
+ ?DOMNode $node = null
+ ): array {
+ $docXpath = new DOMXpath($doc);
+ $nodeList = $docXpath->query($xpath, $node);
+
+ if (!($nodeList instanceof DOMNodeList)) {
+ return [];
+ }
+
+ return iterator_to_array($nodeList);
+ }
+
+ /**
+ * Get the node with the relationship of current node.
+ *
+ * @param DOMNode $node
+ * @param string $relation
+ *
+ * @return DOMNode|null
+ */
+ public static function getRelationNode(DOMNode $node, string $relation)
+ {
+ /** @var DOMNode $node */
+ while (($node = $node->$relation)
+ && $node instanceof DOMNode
+ && $node->nodeType !== XML_DOCUMENT_NODE
+ ) {
+ if ($node->nodeType !== XML_ELEMENT_NODE) {
+ continue;
+ }
+
+ return $node;
+ }
+
+ return null;
+ }
}
diff --git a/src/HtmlDocument.php b/src/HtmlDocument.php
new file mode 100644
index 0000000..f4f8406
--- /dev/null
+++ b/src/HtmlDocument.php
@@ -0,0 +1,115 @@
+doc = $doc;
+ }
+
+ /**
+ * Get DOMDocument
+ *
+ * @return DOMDocument
+ */
+ public function getDoc(): DOMDocument
+ {
+ return $this->doc;
+ }
+
+ /**
+ * Get the outer HTML content.
+ *
+ * @return string|null
+ */
+ public function outerHtml()
+ {
+ return $this->doc->saveHTML();
+ }
+
+ /**
+ * Make the static object can be called as a function.
+ *
+ * @param string $selector
+ *
+ * @return HtmlQuery
+ */
+ public function __invoke(string $selector)
+ {
+ return $this->query($selector);
+ }
+
+ /**
+ * If the parameter is raw html, then create document fragment for it,
+ * If the parameter is a css selector, get the descendants
+ * filtered by a css selector.
+ *
+ * @param string $selector css selector or raw html
+ *
+ * @return HtmlQuery
+ */
+ public function query(string $selector)
+ {
+ if (Helper::isRawHtml($selector)) {
+ $frag = $this->doc->createDocumentFragment();
+ $frag->appendXML($selector);
+
+ return $this->resolve($frag);
+ }
+
+ return $this->find($selector);
+ }
+
+ /**
+ * Get the descendants of document, filtered by a selector.
+ *
+ * @param string $selector
+ *
+ * @return HtmlQuery
+ */
+ public function find(string $selector)
+ {
+ $nodes = Helper::xpathQuery(
+ Helper::toXpath($selector),
+ $this->doc,
+ $this->doc
+ );
+
+ if (Helper::isIdSelector($selector)) {
+ $nodes = $nodes ? $nodes[0] : [];
+ }
+
+ return $this->resolve($nodes);
+ }
+
+ /**
+ * Resolve nodes to HtmlQuery instance.
+ *
+ * @param DOMNode|DOMNode[] $nodes
+ *
+ * @return HtmlQuery
+ */
+ protected function resolve($nodes)
+ {
+ return new HtmlQuery($this->doc, $nodes);
+ }
+}
diff --git a/src/HtmlElement.php b/src/HtmlElement.php
new file mode 100644
index 0000000..d6d0982
--- /dev/null
+++ b/src/HtmlElement.php
@@ -0,0 +1,190 @@
+node = $node;
+ }
+
+ /**
+ * Get the value of an attribute
+ *
+ * @param string $name
+ *
+ * @return string|null
+ */
+ public function getAttr(string $name)
+ {
+ return $this->node->getAttribute($name);
+ }
+
+ /**
+ * Set attribute.
+ *
+ * @param string $name
+ * @param string $value
+ */
+ public function setAttr(string $name, string $value)
+ {
+ $this->node->setAttribute($name, $value);
+ }
+
+ /**
+ * Remove an attribute.
+ *
+ * @param string $attributeName
+ */
+ public function removeAttr(string $attributeName)
+ {
+ $this->node->removeAttribute($attributeName);
+ }
+
+ /**
+ * Remove all attributes except the specified ones.
+ *
+ * @param string|array $except The attribute name(s) that won't be removed
+ */
+ public function removeAllAttrs($except = [])
+ {
+ $names = [];
+ foreach (iterator_to_array($this->node->attributes) as $attribute) {
+ $names[] = $attribute->name;
+ }
+
+ foreach (array_diff($names, (array) $except) as $name) {
+ $this->node->removeAttribute($name);
+ }
+ }
+
+ /**
+ * Determine whether the node has the given attribute.
+ *
+ * @param string $attributeName
+ *
+ * @return bool
+ */
+ public function hasAttr(string $attributeName)
+ {
+ return $this->node->hasAttribute($attributeName);
+ }
+
+ /**
+ * Get the current value of the node.
+ *
+ * @return string|null
+ */
+ public function getVal()
+ {
+ switch ($this->node->tagName) {
+ case 'input':
+ return $this->node->getAttribute('value');
+ case 'textarea':
+ return $this->node->nodeValue;
+ case 'select':
+ return $this->getSelectVal();
+ }
+
+ return null;
+ }
+
+ /**
+ * Set the value of the node.
+ *
+ * @param string $value
+ */
+ public function setVal(string $value)
+ {
+ switch ($this->node->tagName) {
+ case 'input':
+ $this->node->setAttribute('value', $value);
+ break;
+ case 'textarea':
+ $this->node->nodeValue = $value;
+ break;
+ case 'select':
+ $this->setSelectVal($value);
+ break;
+ }
+ }
+
+ /**
+ * Set select hag value
+ *
+ * @param string $value
+ */
+ protected function setSelectVal(string $value)
+ {
+ if ($this->node->tagName == 'select') {
+ $nodes = Helper::xpathQuery(
+ Helper::toXpath('option:selected', 'child::'),
+ $this->getDoc(),
+ $this->node
+ );
+
+ foreach ($nodes as $node) {
+ $node->removeAttribute('selected');
+ }
+
+ $nodes = Helper::xpathQuery(
+ Helper::toXpath("option[value='{$value}']", 'child::'),
+ $this->getDoc(),
+ $this->node
+ );
+
+ if (count($nodes)) {
+ $nodes[0]->setAttribute('selected', 'selected');
+ }
+ }
+ }
+
+ /**
+ * Get select tag value
+ *
+ * @return string|null
+ */
+ protected function getSelectVal()
+ {
+ if ($this->node->tagName === 'select') {
+ $xpaths = [
+ Helper::toXpath('option:selected', 'child::'),
+ 'child::option[1]'
+ ];
+
+ foreach ($xpaths as $xpath) {
+ $nodes = Helper::xpathQuery(
+ $xpath,
+ $this->getDoc(),
+ $this->node
+ );
+
+ if (count($nodes)) {
+ return $nodes[0]->getAttribute('value');
+ }
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/src/HtmlElementCss.php b/src/HtmlElementCss.php
new file mode 100644
index 0000000..acf7932
--- /dev/null
+++ b/src/HtmlElementCss.php
@@ -0,0 +1,209 @@
+hasAttr('class')) {
+ $this->setAttr('class', $className);
+ return;
+ }
+
+ $classNames = Helper::splitClass($className);
+ $class = (string) $this->getAttr('class');
+ $classes = Helper::splitClass($class);
+
+ $classArr = array_diff($classNames, $classes);
+ if (empty($classArr)) {
+ return;
+ }
+
+ $class .= ' ' . implode(' ', $classArr);
+ $this->setAttr('class', $class);
+ }
+
+ /**
+ * Determine whether the node is assigned the given class.
+ *
+ * @param string $className
+ *
+ * @return bool
+ */
+ public function hasClass(string $className)
+ {
+ $class = (string) $this->getAttr('class');
+ $classes = Helper::splitClass($class);
+
+ return in_array($className, $classes);
+ }
+
+ /**
+ * Remove a single class, multiple classes, or all classes.
+ *
+ * @param string|null $className
+ */
+ public function removeClass(?string $className = null)
+ {
+ if (!$this->hasAttr('class')) {
+ return;
+ }
+
+ if (is_null($className)) {
+ $this->removeAttr('class');
+ return;
+ }
+
+ $classNames = Helper::splitClass($className);
+ $class = (string) $this->getAttr('class');
+ $classes = Helper::splitClass($class);
+
+ $classArr = array_diff($classes, $classNames);
+ if (empty($classArr)) {
+ $this->removeAttr('class');
+ return;
+ }
+
+ $class = implode(' ', $classArr);
+ $this->setAttr('class', $class);
+ }
+
+ /**
+ * Add or remove class(es), depending on either the class's presence
+ * or the value of the state argument.
+ *
+ * @param string $className
+ * @param bool|null $state
+ */
+ public function toggleClass(string $className, ?bool $state = null)
+ {
+ if (!is_null($state)) {
+ if ($state) {
+ $this->addClass($className);
+ } else {
+ $this->removeClass($className);
+ }
+ return;
+ }
+
+ if (!$this->hasAttr('class')) {
+ $this->setAttr('class', $className);
+ return;
+ }
+
+ $classNames = Helper::splitClass($className);
+ $classes = Helper::splitClass((string) $this->getAttr('class'));
+
+ $classArr = array_diff($classes, $classNames);
+ $classArr = array_merge(
+ $classArr,
+ array_diff($classNames, $classes)
+ );
+ if (empty($classArr)) {
+ $this->removeClass($className);
+ return;
+ }
+
+ $this->setAttr('class', implode(' ', $classArr));
+ }
+
+ /**
+ * Get the value of a computed style property
+ *
+ * @param string $name
+ *
+ * @return string|null
+ */
+ public function getCss(string $name)
+ {
+ $style = (string) $this->getAttr('style');
+ $css = Helper::splitCss($style);
+ if (!$css) {
+ return null;
+ }
+
+ if (array_key_exists($name, $css)) {
+ return $css[$name];
+ }
+
+ $arr = array_change_key_case($css, CASE_LOWER);
+ $key = strtolower($name);
+ if (array_key_exists($key, $arr)) {
+ return $arr[$key];
+ }
+
+ return null;
+ }
+
+ /**
+ * Set or Remove one CSS property.
+ *
+ * @param string $name
+ * @param string|null $value
+ */
+ public function setCss(string $name, ?string $value)
+ {
+ if ((string) $value === '') {
+ $this->removeCss($name);
+ return;
+ }
+
+ $style = (string) $this->getAttr('style');
+ if (!$style) {
+ $this->setAttr('style', $name . ': ' . $value . ';');
+ return;
+ }
+
+ $css = Helper::splitCss($style);
+ if (!array_key_exists($name, $css)) {
+ $keys = Helper::caseInsensitiveSearch($name, array_keys($css));
+ foreach ($keys as $key) {
+ unset($css[$key]);
+ }
+ }
+
+ $css[$name] = $value;
+ $style = Helper::implodeCss($css);
+ $this->setAttr('style', $style);
+ }
+
+ /**
+ * Remove one CSS property.
+ *
+ * @param string $name
+ */
+ public function removeCss(string $name)
+ {
+ $style = (string) $this->getAttr('style');
+
+ if ($style !== '') {
+ $css = Helper::splitCss($style);
+ $keys = Helper::caseInsensitiveSearch($name, array_keys($css));
+
+ if (!empty($keys)) {
+ foreach ($keys as $key) {
+ unset($css[$key]);
+ }
+
+ $style = Helper::implodeCss($css);
+ $this->setAttr('style', $style);
+ }
+ }
+ }
+}
diff --git a/src/HtmlNode.php b/src/HtmlNode.php
new file mode 100644
index 0000000..07cca34
--- /dev/null
+++ b/src/HtmlNode.php
@@ -0,0 +1,180 @@
+node = $node;
+ }
+
+ /**
+ * Get the outer HTML content.
+ *
+ * @return string|null
+ */
+ public function outerHtml()
+ {
+ return $this->getDoc()->saveHTML($this->node);
+ }
+
+ /**
+ * Get the inner HTML content.
+ *
+ * @return string|null
+ */
+ public function getHtml()
+ {
+ $content = '';
+ foreach (iterator_to_array($this->node->childNodes) as $childNode) {
+ $content .= $this->getDoc()->saveHTML($childNode);
+ }
+
+ return $content;
+ }
+
+ /**
+ * Get the combined text contents, including it's descendants.
+ *
+ * @return string|null
+ */
+ public function getText()
+ {
+ return $this->node->textContent;
+ }
+
+ /**
+ * Set the text contents.
+ *
+ * @param string $text
+ */
+ public function setText(string $text)
+ {
+ $this->node->nodeValue = $text;
+ }
+
+ /**
+ * Remove all child nodes from the DOM.
+ */
+ public function empty()
+ {
+ $this->node->nodeValue = '';
+ }
+
+ /**
+ * Remove the node from the DOM.
+ */
+ public function remove()
+ {
+ if ($this->node->parentNode) {
+ $this->node->parentNode->removeChild($this->node);
+ }
+ }
+
+ /**
+ * Insert a node before the node.
+ *
+ * @param DOMNode $newNode
+ */
+ public function before(DOMNode $newNode)
+ {
+ if ($this->node->parentNode) {
+ $this->node->parentNode->insertBefore($newNode, $this->node);
+ }
+ }
+
+ /**
+ * Insert new node after the node.
+ *
+ * @param DOMNode $newNode
+ */
+ public function after(DOMNode $newNode)
+ {
+ $nextSibling = $this->node->nextSibling;
+
+ if ($nextSibling && $nextSibling->parentNode) {
+ $nextSibling->parentNode->insertBefore($newNode, $nextSibling);
+ } elseif ($this->node->parentNode) {
+ $this->node->parentNode->appendChild($newNode);
+ }
+ }
+
+ /**
+ * Insert a node to the end of the node.
+ *
+ * @param DOMNode $newNode
+ */
+ public function append(DOMNode $newNode)
+ {
+ $this->node->appendChild($newNode);
+ }
+
+ /**
+ * Insert content or node(s) to the beginning of each matched node.
+ *
+ * @param DOMNode $newNode
+ */
+ public function prepend(DOMNode $newNode)
+ {
+ if ($this->node->firstChild) {
+ $this->node->insertBefore($newNode, $this->node->firstChild);
+ } else {
+ $this->node->appendChild($newNode);
+ }
+ }
+
+ /**
+ * Replace the node with the provided node
+ *
+ * @param DOMNode $newNode
+ */
+ public function replaceWith(DOMNode $newNode)
+ {
+ if ($this->node->parentNode) {
+ $this->node->parentNode->replaceChild($newNode, $this->node);
+ }
+ }
+
+ /**
+ * Remove the HTML tag of the node from the DOM.
+ * Leaving the child nodes in their place.
+ */
+ public function unwrapSelf()
+ {
+ foreach (iterator_to_array($this->node->childNodes) as $childNode) {
+ $this->before($childNode);
+ }
+
+ $this->remove();
+ }
+
+ /**
+ * Get DOMDocument of the node
+ *
+ * @return DOMDocument
+ */
+ protected function getDoc(): DOMDocument
+ {
+ return $this->node instanceof DOMDocument
+ ? $this->node
+ : $this->node->ownerDocument;
+ }
+}
diff --git a/src/HtmlQuery.php b/src/HtmlQuery.php
index d74db36..ebfb415 100644
--- a/src/HtmlQuery.php
+++ b/src/HtmlQuery.php
@@ -2,7 +2,7 @@
namespace Sulao\HtmlQuery;
-use DOMDocument, DOMElement, DOMNode, DOMNodeList;
+use DOMDocument, DOMNode, DOMNodeList;
use Traversable;
/**
@@ -10,9 +10,9 @@
*
* @package Sulao\HtmlQuery
*/
-class HtmlQuery extends Selection
+class HtmlQuery extends HtmlQueryNode
{
- const VERSION = '1.0.0';
+ const VERSION = '1.0.1';
/**
* @var DOMDocument
@@ -45,8 +45,8 @@ public function __construct(DOMDocument $doc, $nodes)
*/
public function outerHtml()
{
- return $this->mapFirst(function (DOMNode $node) {
- return $this->doc->saveHTML($node);
+ return $this->mapFirst(function (HtmlNode $node) {
+ return $node->outerHtml();
});
}
@@ -74,13 +74,8 @@ public function html(?string $html = null)
*/
public function getHtml()
{
- return $this->mapFirst(function (DOMNode $node) {
- $content = '';
- foreach (iterator_to_array($node->childNodes) as $childNode) {
- $content .= $this->doc->saveHTML($childNode);
- }
-
- return $content;
+ return $this->mapFirst(function (HtmlNode $node) {
+ return $node->getHtml();
});
}
@@ -127,8 +122,8 @@ public function text(?string $text = null)
*/
public function getText()
{
- return $this->mapFirst(function (DOMNode $node) {
- return $node->textContent;
+ return $this->mapFirst(function (HtmlNode $node) {
+ return $node->getText();
});
}
@@ -141,222 +136,11 @@ public function getText()
*/
public function setText(string $text)
{
- return $this->each(function (DOMNode $node) use ($text) {
- return $node->nodeValue = $text;
- });
- }
-
- /**
- * Get the value of an attribute for the first matched node
- * or set one or more attributes for every matched node.
- *
- * @param string|array $name
- * @param string|null $value
- *
- * @return static|mixed|null
- */
- public function attr($name, $value = null)
- {
- if (is_array($name)) {
- foreach ($name as $key => $val) {
- $this->setAttr($key, $val);
- }
-
- return $this;
- }
-
- if (!is_null($value)) {
- return $this->setAttr($name, $value);
- }
-
- return $this->getAttr($name);
- }
-
- /**
- * Get the value of an attribute for the first matched node
- *
- * @param string $name
- *
- * @return string|null
- */
- public function getAttr(string $name)
- {
- return $this->mapFirst(function (DOMNode $node) use ($name) {
- if (!($node instanceof DOMElement)) {
- return null;
- }
-
- return $node->getAttribute($name);
- });
- }
-
- /**
- * Set one or more attributes for every matched node.
- *
- * @param string $name
- * @param string $value
- *
- * @return static
- */
- public function setAttr(string $name, string $value)
- {
- return $this->each(function (DOMNode $node) use ($name, $value) {
- if ($node instanceof DOMElement) {
- $node->setAttribute($name, $value);
- }
+ return $this->each(function (HtmlNode $node) use ($text) {
+ $node->setText($text);
});
}
- /**
- * Remove an attribute from every matched nodes.
- *
- * @param string $attributeName
- *
- * @return static
- */
- public function removeAttr(string $attributeName)
- {
- return $this->each(function (DOMNode $node) use ($attributeName) {
- if ($node instanceof DOMElement) {
- $node->removeAttribute($attributeName);
- }
- });
- }
-
- /**
- * Remove all attributes from every matched nodes except the specified ones.
- *
- * @param string|array $except The attribute name(s) that won't be removed
- *
- * @return static
- */
- public function removeAllAttrs($except = [])
- {
- return $this->each(function (DOMNode $node) use ($except) {
- $names = [];
- foreach (iterator_to_array($node->attributes) as $attribute) {
- $names[] = $attribute->name;
- }
-
- foreach (array_diff($names, (array) $except) as $name) {
- if ($node instanceof DOMElement) {
- $node->removeAttribute($name);
- }
- }
- });
- }
-
- /**
- * Determine whether any of the nodes have the given attribute.
- *
- * @param string $attributeName
- *
- * @return bool
- */
- public function hasAttr(string $attributeName)
- {
- return $this->mapAnyTrue(
- function (DOMNode $node) use ($attributeName) {
- if (!($node instanceof DOMElement)) {
- return false;
- }
-
- return $node->hasAttribute($attributeName);
- }
- );
- }
-
- /**
- * Alias of attr
- *
- * @param string|array $name
- * @param string|null $value
- *
- * @return static|mixed|null
- */
- public function prop($name, $value = null)
- {
- return $this->attr($name, $value);
- }
-
- /**
- * Alias of removeAttr
- *
- * @param string $attributeName
- *
- * @return static
- */
- public function removeProp(string $attributeName)
- {
- return $this->removeAttr($attributeName);
- }
-
- /**
- * Get the value of an attribute with prefix data- for the first matched
- * node, if the value is valid json string, returns the value encoded in
- * json in appropriate PHP type
- *
- * or set one or more attributes with prefix data- for every matched node.
- *
- * @param string|array $name
- * @param string|null $value
- *
- * @return static|mixed|null
- */
- public function data($name, $value = null)
- {
- if (is_array($name)) {
- $keys = array_keys($name);
- $keys = array_map(function ($value) {
- return 'data-' . $value;
- }, $keys);
-
- $name = array_combine($keys, $name);
- } else {
- $name = 'data-' . $name;
- }
-
- if (!is_null($value) && !is_string($value)) {
- $value = (string) json_encode($value);
- }
-
- $result = $this->attr($name, $value);
-
- if (is_string($result)) {
- $json = json_decode($result);
- if (json_last_error() === JSON_ERROR_NONE) {
- return $json;
- }
- }
-
- return $result;
- }
-
- /**
- * Determine whether any of the nodes have the given attribute
- * prefix with data-.
- *
- * @param string $name
- *
- * @return bool
- */
- public function hasData(string $name)
- {
- return $this->hasAttr('data-' . $name);
- }
-
- /**
- * Remove an attribute prefix with data- from every matched nodes.
- *
- * @param string $name
- *
- * @return static
- */
- public function removeData(string $name)
- {
- return $this->removeAttr('data-' . $name);
- }
-
/**
* Remove all child nodes of all matched nodes from the DOM.
*
@@ -364,8 +148,8 @@ public function removeData(string $name)
*/
public function empty()
{
- return $this->each(function (DOMNode $node) {
- $node->nodeValue = '';
+ return $this->each(function (HtmlNode $node) {
+ $node->empty();
});
}
@@ -382,10 +166,8 @@ public function remove(?string $selector = null)
if (!is_null($selector)) {
$this->filter($selector)->remove();
} else {
- $this->each(function (DOMNode $node) {
- if ($node->parentNode) {
- $node->parentNode->removeChild($node);
- }
+ $this->each(function (HtmlNode $node) {
+ $node->remove();
});
}
@@ -416,32 +198,8 @@ public function val(?string $value = null)
*/
public function getVal()
{
- return $this->mapFirst(function (DOMNode $node) {
- if (!($node instanceof DOMElement)) {
- return null;
- }
-
- switch ($node->tagName) {
- case 'input':
- return $node->getAttribute('value');
- case 'textarea':
- return $node->nodeValue;
- case 'select':
- $ht = $this->resolve($node);
-
- $selected = $ht->children('option:selected');
- if ($selected->count()) {
- return $selected->getAttr('value');
- }
-
- $fistChild = $ht->xpathFind('child::*[1]');
- if ($fistChild->count()) {
- return $fistChild->getAttr('value');
- }
- break;
- }
-
- return null;
+ return $this->mapFirst(function (HtmlElement $node) {
+ return $node->getVal();
});
}
@@ -454,32 +212,8 @@ public function getVal()
*/
public function setVal(string $value)
{
- return $this->each(function (DOMNode $node) use ($value) {
- if (!($node instanceof DOMElement)) {
- return;
- }
-
- switch ($node->tagName) {
- case 'input':
- $node->setAttribute('value', $value);
- break;
- case 'textarea':
- $node->nodeValue = $value;
- break;
- case 'select':
- $ht = $this->resolve($node);
-
- $selected = $ht->children('option:selected');
- if ($selected->count()) {
- $selected->removeAttr('selected');
- }
-
- $options = $ht->children("option[value='{$value}']");
- if ($options->count()) {
- $options->first()->setAttr('selected', 'selected');
- }
- break;
- }
+ return $this->each(function (HtmlElement $node) use ($value) {
+ $node->setVal($value);
});
}
@@ -492,23 +226,8 @@ public function setVal(string $value)
*/
public function addClass(string $className)
{
- return $this->each(function (HtmlQuery $node) use ($className) {
- if (!$node->hasAttr('class')) {
- $node->setAttr('class', $className);
- return;
- }
-
- $classNames = Helper::splitClass($className);
- $class = (string) $node->getAttr('class');
- $classes = Helper::splitClass($class);
-
- $classArr = array_diff($classNames, $classes);
- if (empty($classArr)) {
- return;
- }
-
- $class .= ' ' . implode(' ', $classArr);
- $this->setAttr('class', $class);
+ return $this->each(function (HtmlElement $node) use ($className) {
+ $node->addClass($className);
});
}
@@ -522,15 +241,8 @@ public function addClass(string $className)
public function hasClass(string $className)
{
return $this->mapAnyTrue(
- function (HtmlQuery $node) use ($className) {
- if (!$node->hasAttr('class')) {
- return false;
- }
-
- $class = (string) $node->getAttr('class');
- $classes = Helper::splitClass($class);
-
- return in_array($className, $classes);
+ function (HtmlElement $node) use ($className) {
+ return $node->hasClass($className);
}
);
}
@@ -545,28 +257,8 @@ function (HtmlQuery $node) use ($className) {
*/
public function removeClass(?string $className = null)
{
- return $this->each(function (HtmlQuery $node) use ($className) {
- if (!$node->hasAttr('class')) {
- return;
- }
-
- if (is_null($className)) {
- $node->removeAttr('class');
- return;
- }
-
- $classNames = Helper::splitClass($className);
- $class = (string) $node->getAttr('class');
- $classes = Helper::splitClass($class);
-
- $classArr = array_diff($classes, $classNames);
- if (empty($classArr)) {
- $node->removeAttr('class');
- return;
- }
-
- $class = implode(' ', $classArr);
- $node->setAttr('class', $class);
+ return $this->each(function (HtmlElement $node) use ($className) {
+ $node->removeClass($className);
});
}
@@ -581,35 +273,8 @@ public function removeClass(?string $className = null)
*/
public function toggleClass(string $className, ?bool $state = null)
{
- return $this->each(function (HtmlQuery $node) use ($className, $state) {
- if (!is_null($state)) {
- if ($state) {
- $node->addClass($className);
- } else {
- $node->removeClass($className);
- }
- return;
- }
-
- if (!$this->hasAttr('class')) {
- $node->setAttr('class', $className);
- return;
- }
-
- $classNames = Helper::splitClass($className);
- $classes = Helper::splitClass((string) $this->getAttr('class'));
-
- $classArr = array_diff($classes, $classNames);
- $classArr = array_merge(
- $classArr,
- array_diff($classNames, $classes)
- );
- if (empty($classArr)) {
- $node->removeClass($className);
- return;
- }
-
- $node->setAttr('class', implode(' ', $classArr));
+ return $this->each(function (HtmlElement $node) use ($className, $state) {
+ $node->toggleClass($className, $state);
});
}
@@ -639,7 +304,6 @@ public function css($name, $value = null)
return $this;
}
-
/**
* Get the value of a computed style property for the first matched node
*
@@ -649,24 +313,8 @@ public function css($name, $value = null)
*/
public function getCss(string $name)
{
- return $this->mapFirst(function (HtmlQuery $node) use ($name) {
- $style = (string) $node->attr('style');
- $css = Helper::splitCss($style);
- if (!$css) {
- return null;
- }
-
- if (array_key_exists($name, $css)) {
- return $css[$name];
- }
-
- $arr = array_change_key_case($css, CASE_LOWER);
- $key = strtolower($name);
- if (array_key_exists($key, $arr)) {
- return $arr[$key];
- }
-
- return null;
+ return $this->mapFirst(function (HtmlElement $node) use ($name) {
+ return $node->getCss($name);
});
}
@@ -680,35 +328,8 @@ public function getCss(string $name)
*/
public function setCss(string $name, ?string $value)
{
- return $this->each(function (HtmlQuery $node) use ($name, $value) {
- if ((string) $value === '') {
- $node->removeCss($name);
- return;
- }
-
- $style = (string) $node->attr('style');
- if (!$style) {
- $node->setAttr('style', $name . ': ' . $value . ';');
- return;
- }
-
- $css = Helper::splitCss($style);
- if (!array_key_exists($name, $css)) {
- $allKeys = array_keys($css);
- $arr = array_combine(
- $allKeys,
- array_map('strtolower', $allKeys)
- ) ?: [];
-
- $keys = array_keys($arr, strtolower($name));
- foreach ($keys as $key) {
- unset($css[$key]);
- }
- }
-
- $css[$name] = $value;
- $style = Helper::implodeCss($css);
- $this->setAttr('style', $style);
+ return $this->each(function (HtmlElement $node) use ($name, $value) {
+ $node->setCss($name, $value);
});
}
@@ -721,384 +342,50 @@ public function setCss(string $name, ?string $value)
*/
public function removeCss(string $name)
{
- return $this->each(function (HtmlQuery $node) use ($name) {
- $style = (string) $node->attr('style');
- if (!$style) {
- return;
- }
-
- $css = Helper::splitCss($style);
- $removed = false;
- if (array_key_exists($name, $css)) {
- unset($css[$name]);
- $removed = true;
- } else {
- $allKeys = array_keys($css);
- $arr = array_combine(
- $allKeys,
- array_map('strtolower', $allKeys)
- ) ?: [];
-
- $keys = array_keys($arr, strtolower($name));
- foreach ($keys as $key) {
- unset($css[$key]);
- $removed = true;
- }
- }
-
- if ($removed) {
- $style = Helper::implodeCss($css);
- $this->setAttr('style', $style);
- }
+ return $this->each(function (HtmlElement $node) use ($name) {
+ $node->removeCss($name);
});
}
/**
- * Insert content or node(s) before each matched node.
- *
- * @param string|DOMNode|DOMNode[]|DOMNodeList|static $content
- *
- * @return static
- */
- public function before($content)
- {
- $content = $this->contentResolve($content);
-
- return $this->each(function (DOMNode $node, $index) use ($content) {
- $content->each(function (DOMNode $newNode) use ($node, $index) {
- if ($node->parentNode) {
- $newNode = $index !== $this->count() - 1
- ? $newNode->cloneNode(true)
- : $newNode;
-
- $node->parentNode->insertBefore($newNode, $node);
- }
- });
- });
- }
-
- /**
- * Insert every matched node before the target.
- *
- * @param string|DOMNode|DOMNode[]|DOMNodeList|static $selector
- *
- * @return static
- */
- public function insertBefore($selector)
- {
- $target = $this->targetResolve($selector);
-
- return $target->before($this);
- }
-
- /**
- * Insert content or node(s) after each matched node.
- *
- * @param string|DOMNode|DOMNode[]|DOMNodeList|static $content
- *
- * @return static
- */
- public function after($content)
- {
- $content = $this->contentResolve($content);
-
- return $this->each(function (HtmlQuery $node, $index) use ($content) {
- $content->each(function (DOMNode $newNode) use ($node, $index) {
- $newNode = $index !== $this->count() - 1
- ? $newNode->cloneNode(true)
- : $newNode;
-
- if ($node->next()->count()) {
- $node->next()->before($newNode);
- } else {
- $node->parent()->append($newNode);
- }
- }, true);
- });
- }
-
- /**
- * Insert every matched node after the target.
- *
- * @param string|DOMNode|DOMNode[]DOMNodeList|static $selector
- *
- * @return static
- */
- public function insertAfter($selector)
- {
- $target = $this->targetResolve($selector);
-
- return $target->after($this);
- }
-
- /**
- * Insert content or node(s) to the end of every matched node.
- *
- * @param string|DOMNode|DOMNode[]|DOMNodeList|static $content
- *
- * @return static
- */
- public function append($content)
- {
- $content = $this->contentResolve($content);
-
- return $this->each(function (DOMNode $node, $index) use ($content) {
- $content->each(function (DOMNode $newNode) use ($node, $index) {
- $newNode = $index !== $this->count() - 1
- ? $newNode->cloneNode(true)
- : $newNode;
-
- $node->appendChild($newNode);
- });
- });
- }
-
- /**
- * Insert every matched node to the end of the target.
- *
- * @param string|DOMNode|DOMNode[]DOMNodeList|static $selector
- *
- * @return static
- */
- public function appendTo($selector)
- {
- $target = $this->targetResolve($selector);
-
- return $target->append($this);
- }
-
- /**
- * Insert content or node(s) to the beginning of each matched node.
- *
- * @param string|DOMNode|DOMNode[]|DOMNodeList|static $content
- *
- * @return static
- */
- public function prepend($content)
- {
- $content = $this->contentResolve($content);
-
- return $this->each(function (DOMNode $node, $index) use ($content) {
- $content->each(function (DOMNode $newNode) use ($node, $index) {
- $newNode = $index !== $this->count() - 1
- ? $newNode->cloneNode(true)
- : $newNode;
-
- if ($node->firstChild) {
- $node->insertBefore($newNode, $node->firstChild);
- } else {
- $node->appendChild($newNode);
- }
- }, true);
- });
- }
-
- /**
- * Insert every matched node to the beginning of the target.
- *
- * @param string|DOMNode|DOMNode[]DOMNodeList|static $selector
- *
- * @return static
- */
- public function prependTo($selector)
- {
- $target = $this->targetResolve($selector);
-
- return $target->prepend($this);
- }
-
- /**
- * Replace each matched node with the provided new content or node(s)
- *
- * @param string|DOMNode|DOMNode[]|DOMNodeList|static $content
- *
- * @return static
- */
- public function replaceWith($content)
- {
- $content = $this->contentResolve($content);
- return $this->each(function (DOMNode $node, $index) use ($content) {
- if (!$node->parentNode) {
- return;
- }
-
- $len = $content->count();
- $content->each(
- function (DOMNode $newNode) use ($node, $index, $len) {
- $newNode = $index !== $this->count() - 1
- ? $newNode->cloneNode(true)
- : $newNode;
-
- if ($len === 1) {
- $node->parentNode->replaceChild($newNode, $node);
- } else {
- $this->resolve($newNode)->insertAfter($node);
- }
- },
- true
- );
-
- if ($len !== 1) {
- $node->parentNode->removeChild($node);
- }
- });
- }
-
- /**
- * Replace each target node with the matched node(s)
- *
- * @param string|DOMNode|DOMNode[]DOMNodeList|static $selector
- *
- * @return static
- */
- public function replaceAll($selector)
- {
- $target = $this->targetResolve($selector);
-
- return $target->replaceWith($this);
- }
-
- /**
- * Wrap an HTML structure around each matched node.
- *
- * @param string|DOMNode|DOMNode[]|DOMNodeList|static $content
- *
- * @return static
- */
- public function wrap($content)
- {
- $content = $this->contentResolve($content);
- $newNode = $content[0];
-
- if (empty($newNode)) {
- return $this;
- }
-
- $newNode = $content[0];
-
- return $this->each(function (DOMNode $node, $index) use ($newNode) {
- $newNode = $index !== $this->count() - 1
- ? $newNode->cloneNode(true)
- : $newNode;
-
- $nodes = $this->xpathQuery('descendant::*[last()]', $newNode);
- if (!$nodes) {
- throw new Exception('Invalid wrap html format.');
- }
-
- $deepestNode = end($nodes);
- $node->parentNode->replaceChild($newNode, $node);
- $deepestNode->appendChild($node);
- });
- }
-
- /**
- * Wrap an HTML structure around the content of each matched node.
- *
- * @param string|DOMNode|DOMNode[]|DOMNodeList|static $content
- *
- * @return static
- */
- public function wrapInner($content)
- {
- $content = $this->contentResolve($content);
- $newNode = $content[0];
-
- if (empty($newNode)) {
- return $this;
- }
-
- return $this->each(function (DOMNode $node, $index) use ($newNode) {
- $newNode = $index !== $this->count() - 1
- ? $newNode->cloneNode(true)
- : $newNode;
-
- $nodes = $this->xpathQuery('descendant::*[last()]', $newNode);
- if (!$nodes) {
- throw new Exception('Invalid wrap html format.');
- }
-
- $deepestNode = end($nodes);
-
- // Can't loop $this->node->childNodes directly to append child,
- // Because childNodes will change once appending child.
- foreach (iterator_to_array($node->childNodes) as $childNode) {
- $deepestNode->appendChild($childNode);
- }
-
- $node->appendChild($newNode);
- });
- }
-
- /**
- * Wrap an HTML structure around all matched nodes.
+ * Validate the nodes
*
- * @param string|DOMNode|DOMNode[]|DOMNodeList|static $content
+ * @param DOMNode|DOMNode[]|DOMNodeList|static $nodes
*
- * @return static
+ * @return DOMNode[]
*/
- public function wrapAll($content)
+ protected function validateNodes($nodes)
{
- $content = $this->contentResolve($content);
- if (!$content->count()) {
- return $this;
- }
+ $nodes = $this->convertNodes($nodes);
- $newNode = $content[0];
- $this->each(function (DOMNode $node, $index) use ($newNode) {
- if ($index === 0) {
- $this->resolve($node)->wrap($newNode);
- } else {
- $this->nodes[0]->parentNode->appendChild($node);
+ array_map(function ($node) {
+ if (!($node instanceof DOMNode)) {
+ throw new Exception(
+ 'Expect an instance of DOMNode, '
+ . gettype($node) . ' given.'
+ );
}
- });
-
- return $this;
- }
-
- /**
- * Remove the parents of the matched nodes from the DOM.
- * A optional selector to check the parent node against.
- *
- * @param string|null $selector
- *
- * @return static
- */
- public function unwrap(?string $selector = null)
- {
- return $this->parent($selector)->unwrapSelf();
- }
- /**
- * Remove the HTML tag of the matched nodes from the DOM.
- * Leaving the child nodes in their place.
- *
- * @return static
- */
- public function unwrapSelf()
- {
- return $this->each(function (DOMNode $node) {
- if (!$node->parentNode) {
- return;
- }
+ $document = $node->ownerDocument ?: $node;
- foreach (iterator_to_array($node->childNodes) as $childNode) {
- $node->parentNode->insertBefore($childNode, $node);
+ if ($document !== $this->doc) {
+ throw new Exception(
+ 'The DOMNode does not belong to the DOMDocument.'
+ );
}
+ }, $nodes);
- $node->parentNode->removeChild($node);
- });
+ return $nodes;
}
/**
- * Validate the nodes
+ * Convert nodes to array
*
* @param DOMNode|DOMNode[]|DOMNodeList|static $nodes
*
- * @return DOMNode[]
- * @throws Exception
+ * @return array
*/
- protected function validateNodes($nodes)
+ protected function convertNodes($nodes): array
{
if (empty($nodes)) {
$nodes = [];
@@ -1108,24 +395,6 @@ protected function validateNodes($nodes)
$nodes = [$nodes];
}
- $nodes = Helper::strictArrayUnique($nodes);
- foreach ($nodes as $node) {
- if (!($node instanceof DOMNode)) {
- throw new Exception(
- 'Expect an instance of DOMNode, '
- . gettype($node) . ' given.'
- );
- }
-
- if ((!$node->ownerDocument && $node !== $this->doc)
- || ($node->ownerDocument && $node->ownerDocument !== $this->doc)
- ) {
- throw new Exception(
- 'The DOMNode does not belong to the DOMDocument.'
- );
- }
- }
-
- return $nodes;
+ return Helper::strictArrayUnique($nodes);
}
}
diff --git a/src/HtmlQueryAttribute.php b/src/HtmlQueryAttribute.php
new file mode 100644
index 0000000..64566d1
--- /dev/null
+++ b/src/HtmlQueryAttribute.php
@@ -0,0 +1,202 @@
+ $val) {
+ $this->setAttr($key, $val);
+ }
+
+ return $this;
+ }
+
+ if (!is_null($value)) {
+ return $this->setAttr($name, $value);
+ }
+
+ return $this->getAttr($name);
+ }
+
+ /**
+ * Get the value of an attribute for the first matched node
+ *
+ * @param string $name
+ *
+ * @return string|null
+ */
+ public function getAttr(string $name)
+ {
+ return $this->mapFirst(function (HtmlElement $node) use ($name) {
+ return $node->getAttr($name);
+ });
+ }
+
+ /**
+ * Set one or more attributes for every matched node.
+ *
+ * @param string $name
+ * @param string $value
+ *
+ * @return static
+ */
+ public function setAttr(string $name, string $value)
+ {
+ return $this->each(function (HtmlElement $node) use ($name, $value) {
+ $node->setAttr($name, $value);
+ });
+ }
+
+ /**
+ * Remove an attribute from every matched nodes.
+ *
+ * @param string $attributeName
+ *
+ * @return static
+ */
+ public function removeAttr(string $attributeName)
+ {
+ return $this->each(function (HtmlElement $node) use ($attributeName) {
+ $node->removeAttr($attributeName);
+ });
+ }
+
+ /**
+ * Remove all attributes from every matched nodes except the specified ones.
+ *
+ * @param string|array $except The attribute name(s) that won't be removed
+ *
+ * @return static
+ */
+ public function removeAllAttrs($except = [])
+ {
+ return $this->each(function (HtmlElement $node) use ($except) {
+ $node->removeAllAttrs($except);
+ });
+ }
+
+ /**
+ * Determine whether any of the nodes have the given attribute.
+ *
+ * @param string $attributeName
+ *
+ * @return bool
+ */
+ public function hasAttr(string $attributeName)
+ {
+ return $this->mapAnyTrue(
+ function (HtmlElement $node) use ($attributeName) {
+ return $node->hasAttr($attributeName);
+ }
+ );
+ }
+
+ /**
+ * Alias of attr
+ *
+ * @param string|array $name
+ * @param string|null $value
+ *
+ * @return static|mixed|null
+ */
+ public function prop($name, $value = null)
+ {
+ return $this->attr($name, $value);
+ }
+
+ /**
+ * Alias of removeAttr
+ *
+ * @param string $attributeName
+ *
+ * @return static
+ */
+ public function removeProp(string $attributeName)
+ {
+ return $this->removeAttr($attributeName);
+ }
+
+ /**
+ * Get the value of an attribute with prefix data- for the first matched
+ * node, if the value is valid json string, returns the value encoded in
+ * json in appropriate PHP type
+ *
+ * or set one or more attributes with prefix data- for every matched node.
+ *
+ * @param string|array $name
+ * @param string|array|null $value
+ *
+ * @return static|mixed|null
+ */
+ public function data($name, $value = null)
+ {
+ if (is_array($name)) {
+ array_walk($name, function ($val, $key) {
+ $this->data($key, $val);
+ });
+
+ return $this;
+ }
+
+ $name = 'data-' . $name;
+
+ if (is_null($value)) {
+ $result = $this->getAttr($name);
+
+ $json = json_decode($result);
+ if (json_last_error() === JSON_ERROR_NONE) {
+ return $json;
+ }
+
+ return $result;
+ }
+
+ if (is_array($value)) {
+ $value = (string) json_encode($value);
+ }
+
+ return $this->setAttr($name, $value);
+ }
+
+ /**
+ * Determine whether any of the nodes have the given attribute
+ * prefix with data-.
+ *
+ * @param string $name
+ *
+ * @return bool
+ */
+ public function hasData(string $name)
+ {
+ return $this->hasAttr('data-' . $name);
+ }
+
+ /**
+ * Remove an attribute prefix with data- from every matched nodes.
+ *
+ * @param string $name
+ *
+ * @return static
+ */
+ public function removeData(string $name)
+ {
+ return $this->removeAttr('data-' . $name);
+ }
+}
diff --git a/src/HtmlQueryNode.php b/src/HtmlQueryNode.php
new file mode 100644
index 0000000..95de385
--- /dev/null
+++ b/src/HtmlQueryNode.php
@@ -0,0 +1,325 @@
+contentResolve($content);
+
+ return $this->each(function (HtmlNode $node, $index) use ($content) {
+ $content->each(function (DOMNode $newNode) use ($node, $index) {
+ $newNode = $this->newNode($newNode, $index);
+ $node->before($newNode);
+ });
+ });
+ }
+
+ /**
+ * Insert every matched node before the target.
+ *
+ * @param string|DOMNode|DOMNode[]|DOMNodeList|static $selector
+ *
+ * @return static
+ */
+ public function insertBefore($selector)
+ {
+ $target = $this->targetResolve($selector);
+
+ return $target->before($this);
+ }
+
+ /**
+ * Insert content or node(s) after each matched node.
+ *
+ * @param string|DOMNode|DOMNode[]|DOMNodeList|static $content
+ *
+ * @return static
+ */
+ public function after($content)
+ {
+ $content = $this->contentResolve($content);
+
+ return $this->each(function (HtmlNode $node, $index) use ($content) {
+ $content->each(function (DOMNode $newNode) use ($node, $index) {
+ $newNode = $this->newNode($newNode, $index);
+ $node->after($newNode);
+ }, true);
+ });
+ }
+
+ /**
+ * Insert every matched node after the target.
+ *
+ * @param string|DOMNode|DOMNode[]DOMNodeList|static $selector
+ *
+ * @return static
+ */
+ public function insertAfter($selector)
+ {
+ $target = $this->targetResolve($selector);
+
+ return $target->after($this);
+ }
+
+ /**
+ * Insert content or node(s) to the end of every matched node.
+ *
+ * @param string|DOMNode|DOMNode[]|DOMNodeList|static $content
+ *
+ * @return static
+ */
+ public function append($content)
+ {
+ $content = $this->contentResolve($content);
+
+ return $this->each(function (HtmlNode $node, $index) use ($content) {
+ $content->each(function (DOMNode $newNode) use ($node, $index) {
+ $newNode = $this->newNode($newNode, $index);
+ $node->append($newNode);
+ });
+ });
+ }
+
+ /**
+ * Insert every matched node to the end of the target.
+ *
+ * @param string|DOMNode|DOMNode[]DOMNodeList|static $selector
+ *
+ * @return static
+ */
+ public function appendTo($selector)
+ {
+ $target = $this->targetResolve($selector);
+
+ return $target->append($this);
+ }
+
+ /**
+ * Insert content or node(s) to the beginning of each matched node.
+ *
+ * @param string|DOMNode|DOMNode[]|DOMNodeList|static $content
+ *
+ * @return static
+ */
+ public function prepend($content)
+ {
+ $content = $this->contentResolve($content);
+
+ return $this->each(function (HtmlNode $node, $index) use ($content) {
+ $content->each(function (DOMNode $newNode) use ($node, $index) {
+ $newNode = $this->newNode($newNode, $index);
+ $node->prepend($newNode);
+ }, true);
+ });
+ }
+
+ /**
+ * Insert every matched node to the beginning of the target.
+ *
+ * @param string|DOMNode|DOMNode[]DOMNodeList|static $selector
+ *
+ * @return static
+ */
+ public function prependTo($selector)
+ {
+ $target = $this->targetResolve($selector);
+
+ return $target->prepend($this);
+ }
+
+ /**
+ * Replace each matched node with the provided new content or node(s)
+ *
+ * @param string|DOMNode|DOMNode[]|DOMNodeList|static $content
+ *
+ * @return static
+ */
+ public function replaceWith($content)
+ {
+ $content = $this->contentResolve($content);
+ return $this->each(function (DOMNode $node, $index) use ($content) {
+ if (!$node->parentNode) {
+ return;
+ }
+
+ $len = $content->count();
+ $content->each(
+ function (DOMNode $newNode) use ($node, $index, $len) {
+ $newNode = $this->newNode($newNode, $index);
+
+ if ($len === 1) {
+ $node->parentNode->replaceChild($newNode, $node);
+ } else {
+ $this->resolve($newNode)->insertAfter($node);
+ }
+ },
+ true
+ );
+
+ if ($len !== 1) {
+ $node->parentNode->removeChild($node);
+ }
+ });
+ }
+
+ /**
+ * Replace each target node with the matched node(s)
+ *
+ * @param string|DOMNode|DOMNode[]DOMNodeList|static $selector
+ *
+ * @return static
+ */
+ public function replaceAll($selector)
+ {
+ $target = $this->targetResolve($selector);
+
+ return $target->replaceWith($this);
+ }
+
+ /**
+ * Wrap an HTML structure around each matched node.
+ *
+ * @param string|DOMNode|DOMNode[]|DOMNodeList|static $content
+ *
+ * @return static
+ */
+ public function wrap($content)
+ {
+ $content = $this->contentResolve($content);
+ $newNode = $content[0];
+
+ if (empty($newNode)) {
+ return $this;
+ }
+
+ return $this->each(function (DOMNode $node, $index) use ($newNode) {
+ $newNode = $this->newNode($newNode, $index);
+
+ $nodes = $this->xpathQuery('descendant::*[last()]', $newNode);
+ if (!$nodes) {
+ throw new Exception('Invalid wrap html format.');
+ }
+
+ $deepestNode = end($nodes);
+ $node->parentNode->replaceChild($newNode, $node);
+ $deepestNode->appendChild($node);
+ });
+ }
+
+ /**
+ * Wrap an HTML structure around the content of each matched node.
+ *
+ * @param string|DOMNode|DOMNode[]|DOMNodeList|static $content
+ *
+ * @return static
+ */
+ public function wrapInner($content)
+ {
+ $content = $this->contentResolve($content);
+ $newNode = $content[0];
+
+ if (empty($newNode)) {
+ return $this;
+ }
+
+ return $this->each(function (DOMNode $node, $index) use ($newNode) {
+ $newNode = $this->newNode($newNode, $index);
+
+ $nodes = $this->xpathQuery('descendant::*[last()]', $newNode);
+ if (!$nodes) {
+ throw new Exception('Invalid wrap html format.');
+ }
+
+ $deepestNode = end($nodes);
+
+ foreach (iterator_to_array($node->childNodes) as $childNode) {
+ $deepestNode->appendChild($childNode);
+ }
+
+ $node->appendChild($newNode);
+ });
+ }
+
+ /**
+ * Wrap an HTML structure around all matched nodes.
+ *
+ * @param string|DOMNode|DOMNode[]|DOMNodeList|static $content
+ *
+ * @return static
+ */
+ public function wrapAll($content)
+ {
+ $content = $this->contentResolve($content);
+ if (!$content->count()) {
+ return $this;
+ }
+
+ $newNode = $content[0];
+ $this->each(function (DOMNode $node, $index) use ($newNode) {
+ if ($index === 0) {
+ $this->resolve($node)->wrap($newNode);
+ } else {
+ $this->nodes[0]->parentNode->appendChild($node);
+ }
+ });
+
+ return $this;
+ }
+
+ /**
+ * Remove the parents of the matched nodes from the DOM.
+ * A optional selector to check the parent node against.
+ *
+ * @param string|null $selector
+ *
+ * @return static
+ */
+ public function unwrap(?string $selector = null)
+ {
+ return $this->parent($selector)->unwrapSelf();
+ }
+
+ /**
+ * Remove the HTML tag of the matched nodes from the DOM.
+ * Leaving the child nodes in their place.
+ *
+ * @return static
+ */
+ public function unwrapSelf()
+ {
+ return $this->each(function (HtmlNode $node) {
+ $node->unwrapSelf();
+ });
+ }
+
+ /**
+ * When the selection needs a new node, return the original one or a clone.
+ *
+ * @param DOMNode $newNode
+ * @param int $index
+ *
+ * @return DOMNode
+ */
+ protected function newNode(DOMNode $newNode, int $index)
+ {
+ return $index !== $this->count() - 1
+ ? $newNode->cloneNode(true)
+ : $newNode;
+ }
+}
diff --git a/src/Resolver.php b/src/Resolver.php
new file mode 100644
index 0000000..f866493
--- /dev/null
+++ b/src/Resolver.php
@@ -0,0 +1,174 @@
+doc, $nodes);
+ }
+
+ /**
+ * If the parameter is a css selector, get the descendants
+ * of dom document filtered by the css selector.
+ * If the parameter is selection, resolve that selection to static object.
+ *
+ * @param string|DOMNode|DOMNode[]|DOMNodeList|static $selector
+ *
+ * @return static
+ */
+ protected function targetResolve($selector)
+ {
+ if (is_string($selector)) {
+ return $this->resolve($this->doc)->find($selector);
+ }
+
+ return $this->resolve($selector);
+ }
+
+ /**
+ * If the parameter is string, consider it as raw html,
+ * then create document fragment for it.
+ * If the parameter is selection, resolve that selection to static instance.
+ *
+ * @param string|DOMNode|DOMNode[]|DOMNodeList|static $content
+ *
+ * @return static
+ */
+ protected function contentResolve($content)
+ {
+ if (is_string($content)) {
+ return $this->htmlResolve($content);
+ }
+
+ return $this->resolve($content);
+ }
+
+ /**
+ * Resolve the html content to static instance.
+ *
+ * @param string $html
+ *
+ * @return static
+ */
+ protected function htmlResolve(string $html)
+ {
+ $frag = $this->doc->createDocumentFragment();
+ $frag->appendXML($html);
+
+ return $this->resolve($frag);
+ }
+
+ /**
+ * Resolve the nodes under the relation to static instance.
+ * up to but not including the node matched by the $until selector.
+ *
+ * @param string $relation
+ * @param string|DOMNode|DOMNode[]|DOMNodeList|static $until
+ *
+ * @return static
+ */
+ protected function relationResolve(string $relation, ?string $until = null)
+ {
+ $untilNodes = !is_null($until)
+ ? $this->targetResolve($until)->nodes
+ : [];
+
+ $nodes = [];
+ foreach ($this->nodes as $node) {
+ while ($node = Helper::getRelationNode($node, $relation)) {
+ if (in_array($node, $untilNodes, true)) {
+ break;
+ }
+
+ if (!in_array($node, $nodes, true)) {
+ $nodes[] = $node;
+ }
+ }
+ }
+
+ return $this->resolve($nodes);
+ }
+
+ /**
+ * Resolve the xpath to static instance.
+ *
+ * @param string $xpath
+ *
+ * @return static
+ */
+ protected function xpathResolve(string $xpath)
+ {
+ $nodes = [];
+ foreach ($this->nodes as $node) {
+ $nodes = array_merge($nodes, $this->xpathQuery($xpath, $node));
+ }
+
+ $nodes = Helper::strictArrayUnique($nodes);
+
+ return $this->resolve($nodes);
+ }
+
+ /**
+ * Query xpath to an array of DOMNode
+ *
+ * @param string $xpath
+ * @param DOMNode|null $node
+ *
+ * @return DOMNode[]
+ */
+ protected function xpathQuery(
+ string $xpath,
+ ?DOMNode $node = null
+ ): array {
+ return Helper::xpathQuery($xpath, $this->doc, $node);
+ }
+}
diff --git a/src/Selection.php b/src/Selection.php
index f2529ae..366e02c 100644
--- a/src/Selection.php
+++ b/src/Selection.php
@@ -4,8 +4,9 @@
use ArrayAccess, ArrayIterator;
use Closure, Countable;
-use DOMDocument, DOMNode;
+use DOMDocument, DOMNode, DOMElement;
use IteratorAggregate;
+use ReflectionFunction;
/**
* Class Selection
@@ -46,12 +47,15 @@ public function getNodes(): array
*/
public function each(Closure $function, bool $reverse = false)
{
- $resolve = $this->shouldResolve($function, 0);
+ $class = $this->getClosureClass($function, 0);
$nodes = $reverse ? array_reverse($this->nodes, true) : $this->nodes;
foreach ($nodes as $index => $node) {
- $node = $resolve ? $this->resolve($node) : $node;
- $function($node, $index);
+ $node = $this->closureResolve($class, $node);
+
+ if (!empty($node)) {
+ $function($node, $index);
+ }
}
return $this;
@@ -67,12 +71,12 @@ public function each(Closure $function, bool $reverse = false)
*/
public function map(Closure $function)
{
- $resolve = $this->shouldResolve($function, 0);
+ $class = $this->getClosureClass($function, 0);
$data = [];
foreach ($this->nodes as $index => $node) {
- $node = $resolve ? $this->resolve($node) : $node;
- $data[] = $function($node, $index);
+ $node = $this->closureResolve($class, $node);
+ $data[] = !empty($node) ? $function($node, $index) : null;
}
return $data;
@@ -88,11 +92,11 @@ public function map(Closure $function)
*/
public function mapAnyTrue(Closure $function)
{
- $resolve = $this->shouldResolve($function, 0);
+ $class = $this->getClosureClass($function, 0);
foreach ($this->nodes as $index => $node) {
- $node = $resolve ? $this->resolve($node) : $node;
- if ($function($node, $index)) {
+ $node = $this->closureResolve($class, $node);
+ if (!empty($node) && $function($node, $index)) {
return true;
}
}
@@ -114,10 +118,10 @@ public function mapFirst(Closure $function)
return null;
}
- $resolve = $this->shouldResolve($function, 0);
- $node = $resolve ? $this->resolve($this->nodes[0]) : $this->nodes[0];
+ $class = $this->getClosureClass($function, 0);
+ $node = $this->closureResolve($class, $this->nodes[0]);
- return $function($node);
+ return !empty($node) ? $function($node) : null;
}
/**
@@ -200,8 +204,7 @@ public function offsetSet($offset, $value)
{
if (!($value instanceof DOMNode)) {
throw new Exception(
- 'Expect an instance of DOMNode, '
- . gettype($value) . ' given.'
+ 'Expect an instance of DOMNode, ' . gettype($value) . ' given.'
);
}
@@ -224,4 +227,54 @@ public function offsetGet($offset)
{
return isset($this->nodes[$offset]) ? $this->nodes[$offset] : null;
}
+
+
+
+ /**
+ * Get the class of the specified parameter of the closure.
+ *
+ * @param Closure $function
+ * @param int $index Which parameter of the closure, starts with 0
+ *
+ * @return string
+ */
+ protected function getClosureClass(Closure $function, int $index)
+ {
+ $reflection = new ReflectionFunction($function);
+ $parameters = $reflection->getParameters();
+
+ if (!empty($parameters) && array_key_exists($index, $parameters)) {
+ $class = $parameters[$index]->getClass();
+ if (!empty($class)) {
+ return $class->getName();
+ }
+ }
+
+ return '';
+ }
+
+ /**
+ * Resolve the node to static or HtmlElement instance or leaving it as DOMNode,
+ * Then pass it to closure
+ *
+ * @param string $class
+ * @param DOMNode $node
+ *
+ * @return DOMNode|HtmlElement|HtmlNode|static|null
+ */
+ protected function closureResolve(string $class, DOMNode $node)
+ {
+ if ($class === static::class) {
+ return $this->resolve($node);
+ } elseif ($class === HtmlElement::class) {
+ if (!($node instanceof DOMElement)) {
+ return null;
+ }
+ return new HtmlElement($node);
+ } elseif ($class === HtmlNode::class) {
+ return new HtmlNode($node);
+ }
+
+ return $node;
+ }
}
diff --git a/src/Selector.php b/src/Selector.php
index 62568a4..8c46616 100644
--- a/src/Selector.php
+++ b/src/Selector.php
@@ -3,8 +3,7 @@
namespace Sulao\HtmlQuery;
use Closure;
-use DOMDocument, DOMNode, DOMNodeList, DOMXPath;
-use ReflectionFunction;
+use DOMDocument, DOMNode, DOMNodeList;
/**
* Trait Selector
@@ -13,6 +12,8 @@
*/
trait Selector
{
+ use Resolver;
+
/**
* @var DOMDocument
*/
@@ -23,15 +24,8 @@ trait Selector
*/
protected $nodes;
- /**
- * Selector constructor.
- *
- * @param DOMDocument $doc
- * @param DOMNode|DOMNode[]|DOMNodeList|static $nodes
- *
- * @return static
- */
- abstract public function __construct(DOMDocument $doc, $nodes);
+ abstract protected function getClosureClass(Closure $function, int $index);
+ abstract protected function closureResolve(string $class, DOMNode $node);
/**
* Make the static object can be called as a function.
@@ -78,14 +72,14 @@ public function query(string $selector)
public function find($selector)
{
if (is_string($selector)) {
- $selection = $this->xpathFind(Helper::toXpath($selector));
+ $selection = $this->xpathResolve(Helper::toXpath($selector));
return Helper::isIdSelector($selector)
? $this->resolve($selection->nodes[0] ?? [])
: $selection;
}
- $descendants = $this->xpathFind('descendant::*');
+ $descendants = $this->xpathResolve('descendant::*');
return $descendants->intersect($selector);
}
@@ -101,13 +95,14 @@ public function filter($selector)
{
if (is_string($selector)) {
$xpath = Helper::toXpath($selector, 'self::');
- return $this->xpathFind($xpath);
+ return $this->xpathResolve($xpath);
} elseif ($selector instanceof Closure) {
- $resolve = $this->shouldResolve($selector, 1);
+ $class = $this->getClosureClass($selector, 1);
$nodes = [];
foreach ($this->nodes as $key => $node) {
- if ($selector($key, $resolve ? $this->resolve($node) : $node)) {
+ $resolve = $this->closureResolve($class, $node);
+ if (!empty($resolve) && $selector($key, $resolve)) {
$nodes[] = $node;
}
}
@@ -131,7 +126,7 @@ public function parent(?string $selector = null)
$selector = is_null($selector) ? '*' : $selector;
$xpath = Helper::toXpath($selector, 'parent::');
- return $this->xpathFind($xpath);
+ return $this->xpathResolve($xpath);
}
/**
@@ -147,7 +142,7 @@ public function parents(?string $selector = null)
$selector = is_null($selector) ? '*' : $selector;
$xpath = Helper::toXpath($selector, 'ancestor::');
- return $this->xpathFind($xpath);
+ return $this->xpathResolve($xpath);
}
/**
@@ -176,7 +171,7 @@ public function children(?string $selector = null)
$selector = is_null($selector) ? '*' : $selector;
$xpath = Helper::toXpath($selector, 'child::');
- return $this->xpathFind($xpath);
+ return $this->xpathResolve($xpath);
}
/**
@@ -192,7 +187,7 @@ public function siblings(?string $selector = null)
$xpath = is_null($selector) ? '*' : Helper::toXpath($selector, '');
$xpath = "preceding-sibling::{$xpath}|following-sibling::{$xpath}";
- return $this->xpathFind($xpath);
+ return $this->xpathResolve($xpath);
}
/**
@@ -208,7 +203,7 @@ public function prev(?string $selector = null)
$xpath = is_null($selector) ? '*' : Helper::toXpath($selector, '');
$xpath = "preceding-sibling::{$xpath}[1]";
- return $this->xpathFind($xpath);
+ return $this->xpathResolve($xpath);
}
/**
@@ -224,7 +219,7 @@ public function prevAll(?string $selector = null)
$xpath = is_null($selector) ? '*' : Helper::toXpath($selector, '');
$xpath = "preceding-sibling::{$xpath}";
- return $this->xpathFind($xpath);
+ return $this->xpathResolve($xpath);
}
/**
@@ -253,7 +248,7 @@ public function next(?string $selector = null)
$xpath = is_null($selector) ? '*' : Helper::toXpath($selector, '');
$xpath = "following-sibling::{$xpath}[1]";
- return $this->xpathFind($xpath);
+ return $this->xpathResolve($xpath);
}
/**
@@ -269,7 +264,7 @@ public function nextAll(?string $selector = null)
$xpath = is_null($selector) ? '*' : Helper::toXpath($selector, '');
$xpath = "following-sibling::{$xpath}";
- return $this->xpathFind($xpath);
+ return $this->xpathResolve($xpath);
}
/**
@@ -350,173 +345,4 @@ public function is($selector): bool
return false;
}
-
- /**
- * Resolve DOMNode(s) to a static instance.
- *
- * @param DOMNode|DOMNode[]|DOMNodeList|static $nodes
- *
- * @return static
- */
- protected function resolve($nodes)
- {
- if ($nodes instanceof static) {
- return $nodes;
- }
-
- return new static($this->doc, $nodes);
- }
-
- /**
- * If the parameter is a css selector, get the descendants
- * of dom document filtered by the css selector.
- * If the parameter is selection, resolve that selection to static object.
- *
- * @param string|DOMNode|DOMNode[]|DOMNodeList|static $selector
- *
- * @return static
- */
- protected function targetResolve($selector)
- {
- if (is_string($selector)) {
- return $this->resolve($this->doc)->find($selector);
- }
-
- return $this->resolve($selector);
- }
-
- /**
- * If the parameter is string, consider it as raw html,
- * then create document fragment for it.
- * If the parameter is selection, resolve that selection to static instance.
- *
- * @param string|DOMNode|DOMNode[]|DOMNodeList|static $content
- *
- * @return static
- */
- protected function contentResolve($content)
- {
- if (is_string($content)) {
- return $this->htmlResolve($content);
- }
-
- return $this->resolve($content);
- }
-
- /**
- * Resolve the html content to static instance.
- *
- * @param string $html
- *
- * @return static
- */
- protected function htmlResolve(string $html)
- {
- $frag = $this->doc->createDocumentFragment();
- $frag->appendXML($html);
-
- return $this->resolve($frag);
- }
-
- /**
- * Resolve the nodes under the relation to static instance.
- * up to but not including the node matched by the $until selector.
- *
- * @param string $relation
- * @param string|DOMNode|DOMNode[]|DOMNodeList|static $until
- *
- * @return static
- */
- protected function relationResolve(string $relation, ?string $until = null)
- {
- $until = !is_null($until)
- ? $this->targetResolve($until)->nodes
- : null;
-
- $nodes = [];
- foreach ($this->nodes as $node) {
- while (($node = $node->$relation)
- && $node->nodeType !== XML_DOCUMENT_NODE
- ) {
- if ($node->nodeType !== XML_ELEMENT_NODE) {
- continue;
- }
-
- if (!is_null($until) && $this->resolve($node)->is($until)) {
- break;
- }
-
- if (!in_array($node, $nodes, true)) {
- $nodes[] = $node;
- }
- }
- }
-
- return $this->resolve($nodes);
- }
-
- /**
- * Determine where the parameter of the closure should resolve to static,
- * or just leave it as DOMNode
- *
- * @param Closure $function
- * @param int $index Which parameter of the closure, starts with 0
- *
- * @return bool
- */
- protected function shouldResolve(Closure $function, $index = 0)
- {
- $reflection = new ReflectionFunction($function);
-
- $parameters = $reflection->getParameters();
- if (!empty($parameters) && array_key_exists($index, $parameters)) {
- $class = $parameters[$index]->getClass();
- if ($class && $class->isInstance($this)) {
- return true;
- }
- }
-
- return false;
- }
-
- /**
- * Resolve the xpath to static instance.
- *
- * @param string $xpath
- *
- * @return static
- */
- protected function xpathFind(string $xpath)
- {
- $nodes = [];
- foreach ($this->nodes as $node) {
- $nodes = array_merge($nodes, $this->xpathQuery($xpath, $node));
- }
-
- $nodes = Helper::strictArrayUnique($nodes);
-
- return $this->resolve($nodes);
- }
-
- /**
- * Query xpath to an array of DOMNode
- *
- * @param string $xpath
- * @param DOMNode|null $node
- *
- * @return DOMNode[]
- */
- protected function xpathQuery(
- string $xpath,
- ?DOMNode $node = null
- ): array {
- $docXpath = new DOMXpath($this->doc);
- $nodeList = $docXpath->query($xpath, $node);
-
- if (!($nodeList instanceof DOMNodeList)) {
- return [];
- }
-
- return iterator_to_array($nodeList);
- }
}
diff --git a/tests/AttributeTest.php b/tests/AttributeTest.php
index 1ae5ede..a62a751 100644
--- a/tests/AttributeTest.php
+++ b/tests/AttributeTest.php
@@ -65,8 +65,6 @@ public function testAttr()
100,
$hq->find('img')->eq(1)->setAttr('width', 100)->attr('width')
);
-
- $this->assertNull($hq->attr('none'));
}
public function testRemoveAttr()
@@ -154,8 +152,6 @@ public function testHasAttr()
$this->assertTrue($hq->find('img')->eq(0)->hasAttr('alt'));
$this->assertFalse($hq->find('img')->eq(1)->hasAttr('alt'));
-
- $this->assertFalse($hq->hasAttr('alt'));
}
public function testProp()
@@ -251,6 +247,9 @@ public function testData()
$data = $hq->find('.p-0')->data('content');
$this->assertEquals(1, $data->id);
$this->assertEquals('dom', $data->tag);
+
+ $hq->find('.p-0')->data('json', '{id:1, test:');
+ $this->assertEquals('{id:1, test:', $hq->find('.p-0')->data('json'));
}
public function testHasData()
@@ -513,5 +512,11 @@ public function testCss()
'',
$hq->find('img')->eq(0)->outerHtml()
);
+
+ $hq->find('img')->eq(0)->removeCss('width');
+ $this->assertEquals(
+ '
',
+ $hq->find('img')->eq(0)->outerHtml()
+ );
}
}
diff --git a/tests/ContentTest.php b/tests/ContentTest.php
index 76428bd..ad8efc6 100644
--- a/tests/ContentTest.php
+++ b/tests/ContentTest.php
@@ -1,7 +1,7 @@
find("select[name='type']")->val()
);
- $hq->val('test');
- $this->assertNull($hq->val());
$this->assertNull($hq->find('form')->val());
$html = '
@@ -162,6 +160,9 @@ public function testOuterHtml()
$hq->find('p')->eq(1)->outerHtml(),
'
test2
' ); + + $query = new HtmlQuery($hq->getDoc(), $hq->getDoc()); + $this->assertHtmlEquals($html, $query->outerHtml()); } public function testText() diff --git a/tests/HQTest.php b/tests/HQTest.php index 3f4940d..5d470e8 100644 --- a/tests/HQTest.php +++ b/tests/HQTest.php @@ -1,7 +1,7 @@ assertInstanceOf(HtmlQuery::class, $hq); - $this->assertCount(1, $hq); + $this->assertInstanceOf(HtmlDocument::class, $hq); $this->assertHtmlEquals($html, $hq->outerHtml()); } @@ -30,16 +29,14 @@ public function testHtmlFile() $file = __DIR__ . '/test.html'; $hq = HQ::htmlFile($file); - $this->assertInstanceOf(HtmlQuery::class, $hq); - $this->assertCount(1, $hq); + $this->assertInstanceOf(HtmlDocument::class, $hq); $this->assertHtmlEquals(file_get_contents($file), $hq->outerHtml()); } public function testHtmlInstance() { $hq = HQ::instance(); - $this->assertInstanceOf(HtmlQuery::class, $hq); - $this->assertCount(1, $hq); + $this->assertInstanceOf(HtmlDocument::class, $hq); $this->assertEmpty(trim($hq->outerHtml())); $html = ' @@ -55,8 +52,7 @@ public function testHtmlInstance() '; $hq = HQ::instance($html); - $this->assertInstanceOf(HtmlQuery::class, $hq); - $this->assertCount(1, $hq); + $this->assertInstanceOf(HtmlDocument::class, $hq); $this->assertHtmlEquals($html, $hq->outerHtml()); } } diff --git a/tests/HelperTest.php b/tests/HelperTest.php index 16e67cb..2e66868 100644 --- a/tests/HelperTest.php +++ b/tests/HelperTest.php @@ -108,4 +108,31 @@ public function testIsIdSelector() $this->assertFalse(Helper::isIdSelector('.class')); $this->assertFalse(Helper::isIdSelector('ul > li p')); } + + public function testCaseInsensitiveSearch() + { + $arr = ['needle', 'needle', 'haystack']; + $this->assertEquals( + ['needle', 'needle'], + Helper::caseInsensitiveSearch('needle', $arr) + ); + $this->assertEquals( + ['needle', 'needle'], + Helper::caseInsensitiveSearch('neeDle', $arr) + ); + $this->assertEquals( + [], + Helper::caseInsensitiveSearch('needles', $arr) + ); + + $arr = ['needle', 'NEEDLE', 'haystack']; + $this->assertEquals( + ['needle', 'NEEDLE'], + Helper::caseInsensitiveSearch('needle', $arr) + ); + $this->assertEquals( + ['needle', 'NEEDLE'], + Helper::caseInsensitiveSearch('neeDle', $arr) + ); + } } diff --git a/tests/HtmlNodeTest.php b/tests/HtmlNodeTest.php new file mode 100644 index 0000000..984d2f2 --- /dev/null +++ b/tests/HtmlNodeTest.php @@ -0,0 +1,61 @@ + +html query
html query
foo
foo
foo