loader ajax
This commit is contained in:
+10
@@ -0,0 +1,10 @@
|
||||
/tests export-ignore
|
||||
/.scrutinizar.yml export-ignore
|
||||
/.travis.yml export-ignore
|
||||
/.gitignore export-ignore
|
||||
/CHANGELOG.md export-ignore
|
||||
/CONTRIBUTING.md export-ignore
|
||||
/LICENSE.md export-ignore
|
||||
/README.md export-ignore
|
||||
/phpunit.php export-ignore
|
||||
/phpunit.xml export-ignore
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
filter:
|
||||
paths: [src/*]
|
||||
excluded_paths: [tests/*]
|
||||
checks:
|
||||
php:
|
||||
code_rating: true
|
||||
remove_extra_empty_lines: true
|
||||
remove_php_closing_tag: true
|
||||
remove_trailing_whitespace: true
|
||||
fix_use_statements:
|
||||
remove_unused: true
|
||||
preserve_multiple: false
|
||||
preserve_blanklines: true
|
||||
order_alphabetically: true
|
||||
fix_php_opening_tag: true
|
||||
fix_linefeed: true
|
||||
fix_line_ending: true
|
||||
fix_identation_4spaces: true
|
||||
fix_doc_comments: true
|
||||
tools:
|
||||
external_code_coverage:
|
||||
timeout: 600
|
||||
runs: 3
|
||||
php_code_coverage: false
|
||||
php_code_sniffer:
|
||||
config:
|
||||
standard: PSR2
|
||||
filter:
|
||||
paths: ['src']
|
||||
php_loc:
|
||||
enabled: true
|
||||
excluded_dirs: [vendor, test]
|
||||
php_cpd:
|
||||
enabled: true
|
||||
excluded_dirs: [vendor, test]
|
||||
build:
|
||||
nodes:
|
||||
analysis:
|
||||
tests:
|
||||
override:
|
||||
- php-scrutinizer-run
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "paquettg/php-html-parser",
|
||||
"type": "library",
|
||||
"version": "2.0.2",
|
||||
"description": "An HTML DOM parser. It allows you to manipulate HTML. Find tags on an HTML page with selectors just like jQuery.",
|
||||
"keywords": ["html", "dom", "parser"],
|
||||
"homepage": "https://github.com/paquettg/php-html-parser",
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Gilles Paquette",
|
||||
"email": "paquettg@gmail.com",
|
||||
"homepage": "http://gillespaquette.ca"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": ">=7.1",
|
||||
"ext-mbstring": "*",
|
||||
"paquettg/string-encode": "~1.0.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^7.5.1",
|
||||
"mockery/mockery": "^1.2",
|
||||
"php-coveralls/php-coveralls": "^2.1"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-0": {
|
||||
"PHPHtmlParser": "src/"
|
||||
}
|
||||
},
|
||||
"minimum-stability": "dev",
|
||||
"prefer-stable": true
|
||||
}
|
||||
+254
@@ -0,0 +1,254 @@
|
||||
<?php
|
||||
namespace PHPHtmlParser;
|
||||
|
||||
/**
|
||||
* Class Content
|
||||
*
|
||||
* @package PHPHtmlParser
|
||||
*/
|
||||
class Content
|
||||
{
|
||||
|
||||
/**
|
||||
* The content string.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $content;
|
||||
|
||||
/**
|
||||
* The size of the content.
|
||||
*
|
||||
* @var integer
|
||||
*/
|
||||
protected $size;
|
||||
|
||||
/**
|
||||
* The current position we are in the content.
|
||||
*
|
||||
* @var integer
|
||||
*/
|
||||
protected $pos;
|
||||
|
||||
/**
|
||||
* The following 4 strings are tags that are important to us.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $blank = " \t\r\n";
|
||||
protected $equal = ' =/>';
|
||||
protected $slash = " />\r\n\t";
|
||||
protected $attr = ' >';
|
||||
|
||||
/**
|
||||
* Content constructor.
|
||||
*
|
||||
* @param string $content
|
||||
*/
|
||||
public function __construct(string $content = '')
|
||||
{
|
||||
$this->content = $content;
|
||||
$this->size = strlen($content);
|
||||
$this->pos = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current position of the content.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getPosition(): int
|
||||
{
|
||||
return $this->pos;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current character we are at.
|
||||
*
|
||||
* @param int $char
|
||||
* @return string
|
||||
*/
|
||||
public function char(int $char = null): string
|
||||
{
|
||||
$pos = $this->pos;
|
||||
if ( ! is_null($char)) {
|
||||
$pos = $char;
|
||||
}
|
||||
|
||||
if ( ! isset($this->content[$pos])) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return $this->content[$pos];
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves the current position forward.
|
||||
*
|
||||
* @param int $count
|
||||
* @return Content
|
||||
* @chainable
|
||||
*/
|
||||
public function fastForward(int $count): Content
|
||||
{
|
||||
$this->pos += $count;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves the current position backward.
|
||||
*
|
||||
* @param int $count
|
||||
* @return Content
|
||||
* @chainable
|
||||
*/
|
||||
public function rewind(int $count): Content
|
||||
{
|
||||
$this->pos -= $count;
|
||||
if ($this->pos < 0) {
|
||||
$this->pos = 0;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy the content until we find the given string.
|
||||
*
|
||||
* @param string $string
|
||||
* @param bool $char
|
||||
* @param bool $escape
|
||||
* @return string
|
||||
*/
|
||||
public function copyUntil(string $string, bool $char = false, bool $escape = false): string
|
||||
{
|
||||
if ($this->pos >= $this->size) {
|
||||
// nothing left
|
||||
return '';
|
||||
}
|
||||
|
||||
if ($escape) {
|
||||
$position = $this->pos;
|
||||
$found = false;
|
||||
while ( ! $found) {
|
||||
$position = strpos($this->content, $string, $position);
|
||||
if ($position === false) {
|
||||
// reached the end
|
||||
$found = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($this->char($position - 1) == '\\') {
|
||||
// this character is escaped
|
||||
++$position;
|
||||
continue;
|
||||
}
|
||||
|
||||
$found = true;
|
||||
}
|
||||
} elseif ($char) {
|
||||
$position = strcspn($this->content, $string, $this->pos);
|
||||
$position += $this->pos;
|
||||
} else {
|
||||
$position = strpos($this->content, $string, $this->pos);
|
||||
}
|
||||
|
||||
if ($position === false) {
|
||||
// could not find character, just return the remaining of the content
|
||||
$return = substr($this->content, $this->pos, $this->size - $this->pos);
|
||||
$this->pos = $this->size;
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
if ($position == $this->pos) {
|
||||
// we are at the right place
|
||||
return '';
|
||||
}
|
||||
|
||||
$return = substr($this->content, $this->pos, $position - $this->pos);
|
||||
// set the new position
|
||||
$this->pos = $position;
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies the content until the string is found and return it
|
||||
* unless the 'unless' is found in the substring.
|
||||
*
|
||||
* @param string $string
|
||||
* @param string $unless
|
||||
* @return string
|
||||
*/
|
||||
public function copyUntilUnless(string $string, string $unless)
|
||||
{
|
||||
$lastPos = $this->pos;
|
||||
$this->fastForward(1);
|
||||
$foundString = $this->copyUntil($string, true, true);
|
||||
|
||||
$position = strcspn($foundString, $unless);
|
||||
if ($position == strlen($foundString)) {
|
||||
return $string.$foundString;
|
||||
}
|
||||
// rewind changes and return nothing
|
||||
$this->pos = $lastPos;
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies the content until it reaches the token string.,
|
||||
*
|
||||
* @param string $token
|
||||
* @param bool $char
|
||||
* @param bool $escape
|
||||
* @return string
|
||||
* @uses $this->copyUntil()
|
||||
*/
|
||||
public function copyByToken(string $token, bool $char = false, bool $escape = false)
|
||||
{
|
||||
$string = $this->$token;
|
||||
|
||||
return $this->copyUntil($string, $char, $escape);
|
||||
}
|
||||
|
||||
/**
|
||||
* Skip a given set of characters.
|
||||
*
|
||||
* @param string $string
|
||||
* @param bool $copy
|
||||
* @return Content|string
|
||||
*/
|
||||
public function skip(string $string, bool $copy = false)
|
||||
{
|
||||
$len = strspn($this->content, $string, $this->pos);
|
||||
|
||||
// make it chainable if they don't want a copy
|
||||
$return = $this;
|
||||
if ($copy) {
|
||||
$return = substr($this->content, $this->pos, $len);
|
||||
}
|
||||
|
||||
// update the position
|
||||
$this->pos += $len;
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Skip a given token of pre-defined characters.
|
||||
*
|
||||
* @param string $token
|
||||
* @param bool $copy
|
||||
* @return Content|string
|
||||
* @uses $this->skip()
|
||||
*/
|
||||
public function skipByToken(string $token, bool $copy = false)
|
||||
{
|
||||
$string = $this->$token;
|
||||
|
||||
return $this->skip($string, $copy);
|
||||
}
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
namespace PHPHtmlParser;
|
||||
|
||||
use PHPHtmlParser\Exceptions\CurlException;
|
||||
|
||||
/**
|
||||
* Class Curl
|
||||
*
|
||||
* @package PHPHtmlParser
|
||||
*/
|
||||
class Curl implements CurlInterface
|
||||
{
|
||||
|
||||
/**
|
||||
* A simple curl implementation to get the content of the url.
|
||||
*
|
||||
* @param string $url
|
||||
* @return string
|
||||
* @throws CurlException
|
||||
*/
|
||||
public function get(string $url): string
|
||||
{
|
||||
$ch = curl_init($url);
|
||||
|
||||
if ( ! ini_get('open_basedir')) {
|
||||
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
|
||||
}
|
||||
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
||||
curl_setopt($ch, CURLOPT_VERBOSE, true);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36');
|
||||
curl_setopt($ch, CURLOPT_URL, $url);
|
||||
|
||||
$content = curl_exec($ch);
|
||||
if ($content === false) {
|
||||
// there was a problem
|
||||
$error = curl_error($ch);
|
||||
throw new CurlException('Error retrieving "'.$url.'" ('.$error.')');
|
||||
}
|
||||
|
||||
return $content;
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
namespace PHPHtmlParser;
|
||||
|
||||
/**
|
||||
* Interface CurlInterface
|
||||
*
|
||||
* @package PHPHtmlParser
|
||||
*/
|
||||
interface CurlInterface
|
||||
{
|
||||
|
||||
/**
|
||||
* This method should return the content of the url in a string
|
||||
*
|
||||
* @param string $url
|
||||
* @return string
|
||||
*/
|
||||
public function get(string $url): string;
|
||||
}
|
||||
+782
@@ -0,0 +1,782 @@
|
||||
<?php
|
||||
namespace PHPHtmlParser;
|
||||
|
||||
use PHPHtmlParser\Dom\AbstractNode;
|
||||
use PHPHtmlParser\Dom\HtmlNode;
|
||||
use PHPHtmlParser\Dom\TextNode;
|
||||
use PHPHtmlParser\Exceptions\NotLoadedException;
|
||||
use PHPHtmlParser\Exceptions\StrictException;
|
||||
use stringEncode\Encode;
|
||||
|
||||
/**
|
||||
* Class Dom
|
||||
*
|
||||
* @package PHPHtmlParser
|
||||
*/
|
||||
class Dom
|
||||
{
|
||||
|
||||
/**
|
||||
* The charset we would like the output to be in.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $defaultCharset = 'UTF-8';
|
||||
|
||||
/**
|
||||
* Contains the root node of this dom tree.
|
||||
*
|
||||
* @var HtmlNode
|
||||
*/
|
||||
public $root;
|
||||
|
||||
/**
|
||||
* The raw version of the document string.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $raw;
|
||||
|
||||
/**
|
||||
* The document string.
|
||||
*
|
||||
* @var Content
|
||||
*/
|
||||
protected $content = null;
|
||||
|
||||
/**
|
||||
* The original file size of the document.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $rawSize;
|
||||
|
||||
/**
|
||||
* The size of the document after it is cleaned.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $size;
|
||||
|
||||
/**
|
||||
* A global options array to be used by all load calls.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $globalOptions = [];
|
||||
|
||||
/**
|
||||
* A persistent option object to be used for all options in the
|
||||
* parsing of the file.
|
||||
*
|
||||
* @var Options
|
||||
*/
|
||||
protected $options;
|
||||
|
||||
/**
|
||||
* A list of tags which will always be self closing
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $selfClosing = [
|
||||
'area',
|
||||
'base',
|
||||
'basefont',
|
||||
'br',
|
||||
'col',
|
||||
'embed',
|
||||
'hr',
|
||||
'img',
|
||||
'input',
|
||||
'keygen',
|
||||
'link',
|
||||
'meta',
|
||||
'param',
|
||||
'source',
|
||||
'spacer',
|
||||
'track',
|
||||
'wbr'
|
||||
];
|
||||
|
||||
/**
|
||||
* A list of tags where there should be no /> at the end (html5 style)
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $noSlash = [];
|
||||
|
||||
/**
|
||||
* Returns the inner html of the root node.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->root->innerHtml();
|
||||
}
|
||||
|
||||
/**
|
||||
* A simple wrapper around the root node.
|
||||
*
|
||||
* @param string $name
|
||||
* @return mixed
|
||||
*/
|
||||
public function __get($name)
|
||||
{
|
||||
return $this->root->$name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to load the dom from any resource, string, file, or URL.
|
||||
*
|
||||
* @param string $str
|
||||
* @param array $options
|
||||
* @return Dom
|
||||
* @chainable
|
||||
*/
|
||||
public function load(string $str, array $options = []): Dom
|
||||
{
|
||||
AbstractNode::resetCount();
|
||||
// check if it's a file
|
||||
if (strpos($str, "\n") === false && is_file($str)) {
|
||||
return $this->loadFromFile($str, $options);
|
||||
}
|
||||
// check if it's a url
|
||||
if (preg_match("/^https?:\/\//i", $str)) {
|
||||
return $this->loadFromUrl($str, $options);
|
||||
}
|
||||
|
||||
return $this->loadStr($str, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the dom from a document file/url
|
||||
*
|
||||
* @param string $file
|
||||
* @param array $options
|
||||
* @return Dom
|
||||
* @chainable
|
||||
*/
|
||||
public function loadFromFile(string $file, array $options = []): Dom
|
||||
{
|
||||
return $this->loadStr(file_get_contents($file), $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Use a curl interface implementation to attempt to load
|
||||
* the content from a url.
|
||||
*
|
||||
* @param string $url
|
||||
* @param array $options
|
||||
* @param CurlInterface $curl
|
||||
* @return Dom
|
||||
* @chainable
|
||||
*/
|
||||
public function loadFromUrl(string $url, array $options = [], CurlInterface $curl = null): Dom
|
||||
{
|
||||
if (is_null($curl)) {
|
||||
// use the default curl interface
|
||||
$curl = new Curl;
|
||||
}
|
||||
$content = $curl->get($url);
|
||||
|
||||
return $this->loadStr($content, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parsers the html of the given string. Used for load(), loadFromFile(),
|
||||
* and loadFromUrl().
|
||||
*
|
||||
* @param string $str
|
||||
* @param array $option
|
||||
* @return Dom
|
||||
* @chainable
|
||||
*/
|
||||
public function loadStr(string $str, array $option = []): Dom
|
||||
{
|
||||
$this->options = new Options;
|
||||
$this->options->setOptions($this->globalOptions)
|
||||
->setOptions($option);
|
||||
|
||||
$this->rawSize = strlen($str);
|
||||
$this->raw = $str;
|
||||
|
||||
$html = $this->clean($str);
|
||||
|
||||
$this->size = strlen($str);
|
||||
$this->content = new Content($html);
|
||||
|
||||
$this->parse();
|
||||
$this->detectCharset();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a global options array to be used by all load calls.
|
||||
*
|
||||
* @param array $options
|
||||
* @return Dom
|
||||
* @chainable
|
||||
*/
|
||||
public function setOptions(array $options): Dom
|
||||
{
|
||||
$this->globalOptions = $options;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find elements by css selector on the root node.
|
||||
*
|
||||
* @param string $selector
|
||||
* @param int $nth
|
||||
* @return mixed
|
||||
*/
|
||||
public function find(string $selector, int $nth = null)
|
||||
{
|
||||
$this->isLoaded();
|
||||
|
||||
return $this->root->find($selector, $nth);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find element by Id on the root node
|
||||
*
|
||||
* @param int $id
|
||||
* @return mixed
|
||||
*/
|
||||
public function findById(int $id)
|
||||
{
|
||||
$this->isLoaded();
|
||||
|
||||
return $this->root->findById($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the tag (or tags in an array) to the list of tags that will always
|
||||
* be self closing.
|
||||
*
|
||||
* @param string|array $tag
|
||||
* @return Dom
|
||||
* @chainable
|
||||
*/
|
||||
public function addSelfClosingTag($tag): Dom
|
||||
{
|
||||
if ( ! is_array($tag)) {
|
||||
$tag = [$tag];
|
||||
}
|
||||
foreach ($tag as $value) {
|
||||
$this->selfClosing[] = $value;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the tag (or tags in an array) from the list of tags that will
|
||||
* always be self closing.
|
||||
*
|
||||
* @param string|array $tag
|
||||
* @return Dom
|
||||
* @chainable
|
||||
*/
|
||||
public function removeSelfClosingTag($tag): Dom
|
||||
{
|
||||
if ( ! is_array($tag)) {
|
||||
$tag = [$tag];
|
||||
}
|
||||
$this->selfClosing = array_diff($this->selfClosing, $tag);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the list of self closing tags to empty.
|
||||
*
|
||||
* @return Dom
|
||||
* @chainable
|
||||
*/
|
||||
public function clearSelfClosingTags(): Dom
|
||||
{
|
||||
$this->selfClosing = [];
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Adds a tag to the list of self closing tags that should not have a trailing slash
|
||||
*
|
||||
* @param $tag
|
||||
* @return Dom
|
||||
* @chainable
|
||||
*/
|
||||
public function addNoSlashTag($tag): Dom
|
||||
{
|
||||
if ( ! is_array($tag)) {
|
||||
$tag = [$tag];
|
||||
}
|
||||
foreach ($tag as $value) {
|
||||
$this->noSlash[] = $value;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a tag from the list of no-slash tags.
|
||||
*
|
||||
* @param $tag
|
||||
* @return Dom
|
||||
* @chainable
|
||||
*/
|
||||
public function removeNoSlashTag($tag): Dom
|
||||
{
|
||||
if ( ! is_array($tag)) {
|
||||
$tag = [$tag];
|
||||
}
|
||||
$this->noSlash = array_diff($this->noSlash, $tag);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Empties the list of no-slash tags.
|
||||
*
|
||||
* @return Dom
|
||||
* @chainable
|
||||
*/
|
||||
public function clearNoSlashTags(): Dom
|
||||
{
|
||||
$this->noSlash = [];
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple wrapper function that returns the first child.
|
||||
*
|
||||
* @return \PHPHtmlParser\Dom\AbstractNode
|
||||
*/
|
||||
public function firstChild(): \PHPHtmlParser\Dom\AbstractNode
|
||||
{
|
||||
$this->isLoaded();
|
||||
|
||||
return $this->root->firstChild();
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple wrapper function that returns the last child.
|
||||
*
|
||||
* @return \PHPHtmlParser\Dom\AbstractNode
|
||||
*/
|
||||
public function lastChild(): \PHPHtmlParser\Dom\AbstractNode
|
||||
{
|
||||
$this->isLoaded();
|
||||
|
||||
return $this->root->lastChild();
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple wrapper function that returns count of child elements
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function countChildren(): int
|
||||
{
|
||||
$this->isLoaded();
|
||||
|
||||
return $this->root->countChildren();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get array of children
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getChildren(): array
|
||||
{
|
||||
$this->isLoaded();
|
||||
|
||||
return $this->root->getChildren();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if node have children nodes
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasChildren(): bool
|
||||
{
|
||||
$this->isLoaded();
|
||||
|
||||
return $this->root->hasChildren();
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple wrapper function that returns an element by the
|
||||
* id.
|
||||
*
|
||||
* @param string $id
|
||||
* @return \PHPHtmlParser\Dom\AbstractNode|null
|
||||
*/
|
||||
public function getElementById($id)
|
||||
{
|
||||
$this->isLoaded();
|
||||
|
||||
return $this->find('#'.$id, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple wrapper function that returns all elements by
|
||||
* tag name.
|
||||
*
|
||||
* @param string $name
|
||||
* @return mixed
|
||||
*/
|
||||
public function getElementsByTag(string $name)
|
||||
{
|
||||
$this->isLoaded();
|
||||
|
||||
return $this->find($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple wrapper function that returns all elements by
|
||||
* class name.
|
||||
*
|
||||
* @param string $class
|
||||
* @return mixed
|
||||
*/
|
||||
public function getElementsByClass(string $class)
|
||||
{
|
||||
$this->isLoaded();
|
||||
|
||||
return $this->find('.'.$class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the load methods have been called.
|
||||
*
|
||||
* @throws NotLoadedException
|
||||
*/
|
||||
protected function isLoaded(): void
|
||||
{
|
||||
if (is_null($this->content)) {
|
||||
throw new NotLoadedException('Content is not loaded!');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans the html of any none-html information.
|
||||
*
|
||||
* @param string $str
|
||||
* @return string
|
||||
*/
|
||||
protected function clean(string $str): string
|
||||
{
|
||||
if ($this->options->get('cleanupInput') != true) {
|
||||
// skip entire cleanup step
|
||||
return $str;
|
||||
}
|
||||
|
||||
// remove white space before closing tags
|
||||
$str = mb_eregi_replace("'\s+>", "'>", $str);
|
||||
$str = mb_eregi_replace('"\s+>', '">', $str);
|
||||
|
||||
// clean out the \n\r
|
||||
$replace = ' ';
|
||||
if ($this->options->get('preserveLineBreaks')) {
|
||||
$replace = ' ';
|
||||
}
|
||||
$str = str_replace(["\r\n", "\r", "\n"], $replace, $str);
|
||||
|
||||
// strip the doctype
|
||||
$str = mb_eregi_replace("<!doctype(.*?)>", '', $str);
|
||||
|
||||
// strip out comments
|
||||
$str = mb_eregi_replace("<!--(.*?)-->", '', $str);
|
||||
|
||||
// strip out cdata
|
||||
$str = mb_eregi_replace("<!\[CDATA\[(.*?)\]\]>", '', $str);
|
||||
|
||||
// strip out <script> tags
|
||||
if ($this->options->get('removeScripts') == true) {
|
||||
$str = mb_eregi_replace("<\s*script[^>]*[^/]>(.*?)<\s*/\s*script\s*>", '', $str);
|
||||
$str = mb_eregi_replace("<\s*script\s*>(.*?)<\s*/\s*script\s*>", '', $str);
|
||||
}
|
||||
|
||||
// strip out <style> tags
|
||||
if ($this->options->get('removeStyles') == true) {
|
||||
$str = mb_eregi_replace("<\s*style[^>]*[^/]>(.*?)<\s*/\s*style\s*>", '', $str);
|
||||
$str = mb_eregi_replace("<\s*style\s*>(.*?)<\s*/\s*style\s*>", '', $str);
|
||||
}
|
||||
|
||||
// strip out server side scripts
|
||||
if ($this->options->get('serverSideScriptis') == true){
|
||||
$str = mb_eregi_replace("(<\?)(.*?)(\?>)", '', $str);
|
||||
}
|
||||
|
||||
// strip smarty scripts
|
||||
$str = mb_eregi_replace("(\{\w)(.*?)(\})", '', $str);
|
||||
|
||||
return $str;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to parse the html in content.
|
||||
*/
|
||||
protected function parse(): void
|
||||
{
|
||||
// add the root node
|
||||
$this->root = new HtmlNode('root');
|
||||
$activeNode = $this->root;
|
||||
while ( ! is_null($activeNode)) {
|
||||
$str = $this->content->copyUntil('<');
|
||||
if ($str == '') {
|
||||
$info = $this->parseTag();
|
||||
if ( ! $info['status']) {
|
||||
// we are done here
|
||||
$activeNode = null;
|
||||
continue;
|
||||
}
|
||||
|
||||
// check if it was a closing tag
|
||||
if ($info['closing']) {
|
||||
$foundOpeningTag = true;
|
||||
$originalNode = $activeNode;
|
||||
while ($activeNode->getTag()->name() != $info['tag']) {
|
||||
$activeNode = $activeNode->getParent();
|
||||
if (is_null($activeNode)) {
|
||||
// we could not find opening tag
|
||||
$activeNode = $originalNode;
|
||||
$foundOpeningTag = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($foundOpeningTag) {
|
||||
$activeNode = $activeNode->getParent();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( ! isset($info['node'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/** @var AbstractNode $node */
|
||||
$node = $info['node'];
|
||||
$activeNode->addChild($node);
|
||||
|
||||
// check if node is self closing
|
||||
if ( ! $node->getTag()->isSelfClosing()) {
|
||||
$activeNode = $node;
|
||||
}
|
||||
} else if ($this->options->whitespaceTextNode ||
|
||||
trim($str) != ''
|
||||
) {
|
||||
// we found text we care about
|
||||
$textNode = new TextNode($str, $this->options->removeDoubleSpace);
|
||||
$activeNode->addChild($textNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to parse a tag out of the content.
|
||||
*
|
||||
* @return array
|
||||
* @throws StrictException
|
||||
*/
|
||||
protected function parseTag(): array
|
||||
{
|
||||
$return = [
|
||||
'status' => false,
|
||||
'closing' => false,
|
||||
'node' => null,
|
||||
];
|
||||
if ($this->content->char() != '<') {
|
||||
// we are not at the beginning of a tag
|
||||
return $return;
|
||||
}
|
||||
|
||||
// check if this is a closing tag
|
||||
if ($this->content->fastForward(1)->char() == '/') {
|
||||
// end tag
|
||||
$tag = $this->content->fastForward(1)
|
||||
->copyByToken('slash', true);
|
||||
// move to end of tag
|
||||
$this->content->copyUntil('>');
|
||||
$this->content->fastForward(1);
|
||||
|
||||
// check if this closing tag counts
|
||||
$tag = strtolower($tag);
|
||||
if (in_array($tag, $this->selfClosing)) {
|
||||
$return['status'] = true;
|
||||
|
||||
return $return;
|
||||
} else {
|
||||
$return['status'] = true;
|
||||
$return['closing'] = true;
|
||||
$return['tag'] = strtolower($tag);
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
$tag = strtolower($this->content->copyByToken('slash', true));
|
||||
$node = new HtmlNode($tag);
|
||||
|
||||
// attributes
|
||||
while ($this->content->char() != '>' &&
|
||||
$this->content->char() != '/') {
|
||||
$space = $this->content->skipByToken('blank', true);
|
||||
if (empty($space)) {
|
||||
$this->content->fastForward(1);
|
||||
continue;
|
||||
}
|
||||
|
||||
$name = $this->content->copyByToken('equal', true);
|
||||
if ($name == '/') {
|
||||
break;
|
||||
}
|
||||
|
||||
if (empty($name)) {
|
||||
$this->content->skipByToken('blank');
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->content->skipByToken('blank');
|
||||
if ($this->content->char() == '=') {
|
||||
$attr = [];
|
||||
$this->content->fastForward(1)
|
||||
->skipByToken('blank');
|
||||
switch ($this->content->char()) {
|
||||
case '"':
|
||||
$attr['doubleQuote'] = true;
|
||||
$this->content->fastForward(1);
|
||||
$string = $this->content->copyUntil('"', true, true);
|
||||
do {
|
||||
$moreString = $this->content->copyUntilUnless('"', '=>');
|
||||
$string .= $moreString;
|
||||
} while ( ! empty($moreString));
|
||||
$attr['value'] = $string;
|
||||
$this->content->fastForward(1);
|
||||
$node->getTag()->$name = $attr;
|
||||
break;
|
||||
case "'":
|
||||
$attr['doubleQuote'] = false;
|
||||
$this->content->fastForward(1);
|
||||
$string = $this->content->copyUntil("'", true, true);
|
||||
do {
|
||||
$moreString = $this->content->copyUntilUnless("'", '=>');
|
||||
$string .= $moreString;
|
||||
} while ( ! empty($moreString));
|
||||
$attr['value'] = $string;
|
||||
$this->content->fastForward(1);
|
||||
$node->getTag()->$name = $attr;
|
||||
break;
|
||||
default:
|
||||
$attr['doubleQuote'] = true;
|
||||
$attr['value'] = $this->content->copyByToken('attr', true);
|
||||
$node->getTag()->$name = $attr;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
// no value attribute
|
||||
if ($this->options->strict) {
|
||||
// can't have this in strict html
|
||||
$character = $this->content->getPosition();
|
||||
throw new StrictException("Tag '$tag' has an attribute '$name' with out a value! (character #$character)");
|
||||
}
|
||||
$node->getTag()->$name = [
|
||||
'value' => null,
|
||||
'doubleQuote' => true,
|
||||
];
|
||||
if ($this->content->char() != '>') {
|
||||
$this->content->rewind(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->content->skipByToken('blank');
|
||||
if ($this->content->char() == '/') {
|
||||
// self closing tag
|
||||
$node->getTag()->selfClosing();
|
||||
$this->content->fastForward(1);
|
||||
} elseif (in_array($tag, $this->selfClosing)) {
|
||||
|
||||
// Should be a self closing tag, check if we are strict
|
||||
if ($this->options->strict) {
|
||||
$character = $this->content->getPosition();
|
||||
throw new StrictException("Tag '$tag' is not self closing! (character #$character)");
|
||||
}
|
||||
|
||||
// We force self closing on this tag.
|
||||
$node->getTag()->selfClosing();
|
||||
|
||||
// Should this tag use a trailing slash?
|
||||
if(in_array($tag, $this->noSlash))
|
||||
{
|
||||
$node->getTag()->noTrailingSlash();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
$this->content->fastForward(1);
|
||||
|
||||
$return['status'] = true;
|
||||
$return['node'] = $node;
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to detect the charset that the html was sent in.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function detectCharset(): bool
|
||||
{
|
||||
// set the default
|
||||
$encode = new Encode;
|
||||
$encode->from($this->defaultCharset);
|
||||
$encode->to($this->defaultCharset);
|
||||
|
||||
if ( ! is_null($this->options->enforceEncoding)) {
|
||||
// they want to enforce the given encoding
|
||||
$encode->from($this->options->enforceEncoding);
|
||||
$encode->to($this->options->enforceEncoding);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$meta = $this->root->find('meta[http-equiv=Content-Type]', 0);
|
||||
if (is_null($meta)) {
|
||||
// could not find meta tag
|
||||
$this->root->propagateEncoding($encode);
|
||||
|
||||
return false;
|
||||
}
|
||||
$content = $meta->content;
|
||||
if (empty($content)) {
|
||||
// could not find content
|
||||
$this->root->propagateEncoding($encode);
|
||||
|
||||
return false;
|
||||
}
|
||||
$matches = [];
|
||||
if (preg_match('/charset=(.+)/', $content, $matches)) {
|
||||
$encode->from(trim($matches[1]));
|
||||
$this->root->propagateEncoding($encode);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// no charset found
|
||||
$this->root->propagateEncoding($encode);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
+505
@@ -0,0 +1,505 @@
|
||||
<?php
|
||||
namespace PHPHtmlParser\Dom;
|
||||
|
||||
use PHPHtmlParser\Exceptions\CircularException;
|
||||
use PHPHtmlParser\Exceptions\ParentNotFoundException;
|
||||
use PHPHtmlParser\Exceptions\ChildNotFoundException;
|
||||
use PHPHtmlParser\Selector\Selector;
|
||||
use PHPHtmlParser\Selector\Parser as SelectorParser;
|
||||
use stringEncode\Encode;
|
||||
use PHPHtmlParser\Finder;
|
||||
|
||||
/**
|
||||
* Dom node object.
|
||||
*
|
||||
* @property string outerhtml
|
||||
* @property string innerhtml
|
||||
* @property string text
|
||||
* @property int prev
|
||||
* @property int next
|
||||
* @property \PHPHtmlParser\Dom\Tag tag
|
||||
* @property InnerNode parent
|
||||
*/
|
||||
abstract class AbstractNode
|
||||
{
|
||||
private static $count = 0;
|
||||
/**
|
||||
* Contains the tag name/type
|
||||
*
|
||||
* @var \PHPHtmlParser\Dom\Tag
|
||||
*/
|
||||
protected $tag;
|
||||
|
||||
/**
|
||||
* Contains a list of attributes on this tag.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $attr = [];
|
||||
|
||||
/**
|
||||
* Contains the parent Node.
|
||||
*
|
||||
* @var InnerNode
|
||||
*/
|
||||
protected $parent = null;
|
||||
|
||||
/**
|
||||
* The unique id of the class. Given by PHP.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $id;
|
||||
|
||||
/**
|
||||
* The encoding class used to encode strings.
|
||||
*
|
||||
* @var mixed
|
||||
*/
|
||||
protected $encode;
|
||||
|
||||
/**
|
||||
* An array of all the children.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $children = [];
|
||||
|
||||
/**
|
||||
* Creates a unique id for this node.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->id = self::$count;
|
||||
self::$count++;
|
||||
}
|
||||
|
||||
/**
|
||||
* Magic get method for attributes and certain methods.
|
||||
*
|
||||
* @param string $key
|
||||
* @return mixed
|
||||
*/
|
||||
public function __get(string $key)
|
||||
{
|
||||
// check attribute first
|
||||
if ( ! is_null($this->getAttribute($key))) {
|
||||
return $this->getAttribute($key);
|
||||
}
|
||||
switch (strtolower($key)) {
|
||||
case 'outerhtml':
|
||||
return $this->outerHtml();
|
||||
case 'innerhtml':
|
||||
return $this->innerHtml();
|
||||
case 'text':
|
||||
return $this->text();
|
||||
case 'tag':
|
||||
return $this->getTag();
|
||||
case 'parent':
|
||||
return $this->getParent();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to clear out any object references.
|
||||
*/
|
||||
public function __destruct()
|
||||
{
|
||||
$this->tag = null;
|
||||
$this->attr = [];
|
||||
$this->parent = null;
|
||||
$this->children = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Simply calls the outer text method.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
return $this->outerHtml();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset node counter
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function resetCount()
|
||||
{
|
||||
self::$count = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the id of this object.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function id(): int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the parent of node.
|
||||
*
|
||||
* @return AbstractNode
|
||||
*/
|
||||
public function getParent()
|
||||
{
|
||||
return $this->parent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the parent node.
|
||||
*
|
||||
* @param InnerNode $parent
|
||||
* @return AbstractNode
|
||||
* @throws CircularException
|
||||
* @chainable
|
||||
*/
|
||||
public function setParent(InnerNode $parent): AbstractNode
|
||||
{
|
||||
// remove from old parent
|
||||
if ( ! is_null($this->parent)) {
|
||||
if ($this->parent->id() == $parent->id()) {
|
||||
// already the parent
|
||||
return $this;
|
||||
}
|
||||
|
||||
$this->parent->removeChild($this->id);
|
||||
}
|
||||
|
||||
$this->parent = $parent;
|
||||
|
||||
// assign child to parent
|
||||
$this->parent->addChild($this);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes this node and all its children from the
|
||||
* DOM tree.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function delete()
|
||||
{
|
||||
if ( ! is_null($this->parent)) {
|
||||
$this->parent->removeChild($this->id);
|
||||
}
|
||||
$this->parent->clear();
|
||||
$this->clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the encoding class to this node.
|
||||
*
|
||||
* @param Encode $encode
|
||||
* @return void
|
||||
*/
|
||||
public function propagateEncoding(Encode $encode)
|
||||
{
|
||||
$this->encode = $encode;
|
||||
$this->tag->setEncoding($encode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given node id is an ancestor of
|
||||
* the current node.
|
||||
*
|
||||
* @param int $id
|
||||
* @return bool
|
||||
*/
|
||||
public function isAncestor(int $id): Bool
|
||||
{
|
||||
if ( ! is_null($this->getAncestor($id))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to get an ancestor node by the given id.
|
||||
*
|
||||
* @param int $id
|
||||
* @return null|AbstractNode
|
||||
*/
|
||||
public function getAncestor(int $id)
|
||||
{
|
||||
if ( ! is_null($this->parent)) {
|
||||
if ($this->parent->id() == $id) {
|
||||
return $this->parent;
|
||||
}
|
||||
|
||||
return $this->parent->getAncestor($id);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the current node has a next sibling.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasNextSibling(): bool
|
||||
{
|
||||
try
|
||||
{
|
||||
$this->nextSibling();
|
||||
|
||||
// sibling found, return true;
|
||||
return true;
|
||||
}
|
||||
catch (ParentNotFoundException $e)
|
||||
{
|
||||
// no parent, no next sibling
|
||||
return false;
|
||||
}
|
||||
catch (ChildNotFoundException $e)
|
||||
{
|
||||
// no sibling found
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to get the next sibling.
|
||||
*
|
||||
* @return AbstractNode
|
||||
* @throws ParentNotFoundException
|
||||
*/
|
||||
public function nextSibling(): AbstractNode
|
||||
{
|
||||
if (is_null($this->parent)) {
|
||||
throw new ParentNotFoundException('Parent is not set for this node.');
|
||||
}
|
||||
|
||||
return $this->parent->nextChild($this->id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to get the previous sibling
|
||||
*
|
||||
* @return AbstractNode
|
||||
* @throws ParentNotFoundException
|
||||
*/
|
||||
public function previousSibling(): AbstractNode
|
||||
{
|
||||
if (is_null($this->parent)) {
|
||||
throw new ParentNotFoundException('Parent is not set for this node.');
|
||||
}
|
||||
|
||||
return $this->parent->previousChild($this->id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the tag object of this node.
|
||||
*
|
||||
* @return Tag
|
||||
*/
|
||||
public function getTag(): Tag
|
||||
{
|
||||
return $this->tag;
|
||||
}
|
||||
|
||||
/**
|
||||
* A wrapper method that simply calls the getAttribute method
|
||||
* on the tag of this node.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getAttributes(): array
|
||||
{
|
||||
$attributes = $this->tag->getAttributes();
|
||||
foreach ($attributes as $name => $info) {
|
||||
$attributes[$name] = $info['value'];
|
||||
}
|
||||
|
||||
return $attributes;
|
||||
}
|
||||
|
||||
/**
|
||||
* A wrapper method that simply calls the getAttribute method
|
||||
* on the tag of this node.
|
||||
*
|
||||
* @param string $key
|
||||
* @return mixed
|
||||
*/
|
||||
public function getAttribute(string $key)
|
||||
{
|
||||
$attribute = $this->tag->getAttribute($key);
|
||||
if ( ! is_null($attribute)) {
|
||||
$attribute = $attribute['value'];
|
||||
}
|
||||
|
||||
return $attribute;
|
||||
}
|
||||
|
||||
/**
|
||||
* A wrapper method that simply calls the hasAttribute method
|
||||
* on the tag of this node.
|
||||
*
|
||||
* @param string $key
|
||||
* @return bool
|
||||
*/
|
||||
public function hasAttribute(string $key): bool
|
||||
{
|
||||
return $this->tag->hasAttribute($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* A wrapper method that simply calls the setAttribute method
|
||||
* on the tag of this node.
|
||||
*
|
||||
* @param string $key
|
||||
* @param string|null $value
|
||||
* @return AbstractNode
|
||||
* @chainable
|
||||
*/
|
||||
public function setAttribute(string $key, $value): AbstractNode
|
||||
{
|
||||
$this->tag->setAttribute($key, $value);
|
||||
|
||||
//clear any cache
|
||||
$this->clear();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* A wrapper method that simply calls the removeAttribute method
|
||||
* on the tag of this node.
|
||||
*
|
||||
* @param string $key
|
||||
* @return void
|
||||
*/
|
||||
public function removeAttribute(string $key): void
|
||||
{
|
||||
$this->tag->removeAttribute($key);
|
||||
|
||||
//clear any cache
|
||||
$this->clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* A wrapper method that simply calls the removeAllAttributes
|
||||
* method on the tag of this node.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function removeAllAttributes(): void
|
||||
{
|
||||
$this->tag->removeAllAttributes();
|
||||
|
||||
//clear any cache
|
||||
$this->clear();
|
||||
}
|
||||
/**
|
||||
* Function to locate a specific ancestor tag in the path to the root.
|
||||
*
|
||||
* @param string $tag
|
||||
* @return AbstractNode
|
||||
* @throws ParentNotFoundException
|
||||
*/
|
||||
public function ancestorByTag(string $tag): AbstractNode
|
||||
{
|
||||
// Start by including ourselves in the comparison.
|
||||
$node = $this;
|
||||
|
||||
while ( ! is_null($node)) {
|
||||
if ($node->tag->name() == $tag) {
|
||||
return $node;
|
||||
}
|
||||
|
||||
$node = $node->getParent();
|
||||
}
|
||||
|
||||
throw new ParentNotFoundException('Could not find an ancestor with "'.$tag.'" tag');
|
||||
}
|
||||
|
||||
/**
|
||||
* Find elements by css selector
|
||||
*
|
||||
* @param string $selector
|
||||
* @param int $nth
|
||||
* @return mixed
|
||||
*/
|
||||
public function find(string $selector, int $nth = null)
|
||||
{
|
||||
$selector = new Selector($selector, new SelectorParser());
|
||||
$nodes = $selector->find($this);
|
||||
|
||||
if ( ! is_null($nth)) {
|
||||
// return nth-element or array
|
||||
if (isset($nodes[$nth])) {
|
||||
return $nodes[$nth];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return $nodes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find node by id
|
||||
*
|
||||
* @param int $id
|
||||
* @return bool|AbstractNode
|
||||
*/
|
||||
public function findById(int $id)
|
||||
{
|
||||
$finder= new Finder($id);
|
||||
|
||||
return $finder->find($this);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets the inner html of this node.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
abstract public function innerHtml(): string;
|
||||
|
||||
/**
|
||||
* Gets the html of this node, including it's own
|
||||
* tag.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
abstract public function outerHtml(): string;
|
||||
|
||||
/**
|
||||
* Gets the text of this node (if there is any text).
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
abstract public function text(): string;
|
||||
|
||||
/**
|
||||
* Call this when something in the node tree has changed. Like a child has been added
|
||||
* or a parent has been changed.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
abstract protected function clear(): void;
|
||||
|
||||
/**
|
||||
* Check is node type textNode
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function isTextNode(): bool
|
||||
{
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
namespace PHPHtmlParser\Dom;
|
||||
|
||||
use Countable;
|
||||
use ArrayIterator;
|
||||
use IteratorAggregate;
|
||||
|
||||
/**
|
||||
* Dom node object which will allow users to use it as
|
||||
* an array.
|
||||
*/
|
||||
abstract class ArrayNode extends AbstractNode implements IteratorAggregate, Countable
|
||||
{
|
||||
|
||||
/**
|
||||
* Gets the iterator
|
||||
*
|
||||
* @return ArrayIterator
|
||||
*/
|
||||
public function getIterator(): ArrayIterator
|
||||
{
|
||||
return new ArrayIterator($this->getIteratorArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the count of the iterator array.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function count(): int
|
||||
{
|
||||
return count($this->getIteratorArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the array to be used the the iterator.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
abstract protected function getIteratorArray(): array;
|
||||
}
|
||||
+168
@@ -0,0 +1,168 @@
|
||||
<?php
|
||||
namespace PHPHtmlParser\Dom;
|
||||
|
||||
use ArrayAccess;
|
||||
use ArrayIterator;
|
||||
use Countable;
|
||||
use IteratorAggregate;
|
||||
use PHPHtmlParser\Exceptions\EmptyCollectionException;
|
||||
|
||||
/**
|
||||
* Class Collection
|
||||
*
|
||||
* @package PHPHtmlParser\Dom
|
||||
*/
|
||||
class Collection implements IteratorAggregate, ArrayAccess, Countable
|
||||
{
|
||||
|
||||
/**
|
||||
* The collection of Nodes.
|
||||
*
|
||||
* @param array
|
||||
*/
|
||||
protected $collection = [];
|
||||
|
||||
/**
|
||||
* Attempts to call the method on the first node in
|
||||
* the collection.
|
||||
*
|
||||
* @param string $method
|
||||
* @param array $arguments
|
||||
* @return mixed
|
||||
* @throws EmptyCollectionException
|
||||
*/
|
||||
public function __call(string $method, array $arguments)
|
||||
{
|
||||
$node = reset($this->collection);
|
||||
if ($node instanceof AbstractNode) {
|
||||
return call_user_func_array([$node, $method], $arguments);
|
||||
} else {
|
||||
throw new EmptyCollectionException('The collection does not contain any Nodes.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to apply the magic get to the first node
|
||||
* in the collection.
|
||||
*
|
||||
* @param mixed $key
|
||||
* @return mixed
|
||||
* @throws EmptyCollectionException
|
||||
*/
|
||||
public function __get($key)
|
||||
{
|
||||
$node = reset($this->collection);
|
||||
if ($node instanceof AbstractNode) {
|
||||
return $node->$key;
|
||||
} else {
|
||||
throw new EmptyCollectionException('The collection does not contain any Nodes.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the magic string method to the first node in
|
||||
* the collection.
|
||||
*
|
||||
* @return string
|
||||
* @throws EmptyCollectionException
|
||||
*/
|
||||
public function __toString(): string
|
||||
{
|
||||
$node = reset($this->collection);
|
||||
if ($node instanceof AbstractNode) {
|
||||
return (string)$node;
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the count of the collection.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function count(): int
|
||||
{
|
||||
return count($this->collection);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an iterator for the collection.
|
||||
*
|
||||
* @return ArrayIterator
|
||||
*/
|
||||
public function getIterator(): ArrayIterator
|
||||
{
|
||||
return new ArrayIterator($this->collection);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set an attribute by the given offset
|
||||
*
|
||||
* @param mixed $offset
|
||||
* @param mixed $value
|
||||
*/
|
||||
public function offsetSet($offset, $value): void
|
||||
{
|
||||
if (is_null($offset)) {
|
||||
$this->collection[] = $value;
|
||||
} else {
|
||||
$this->collection[$offset] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if an offset exists.
|
||||
*
|
||||
* @param mixed $offset
|
||||
* @return bool
|
||||
*/
|
||||
public function offsetExists($offset): bool
|
||||
{
|
||||
return isset($this->collection[$offset]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unset a collection Node.
|
||||
*
|
||||
* @param mixed $offset
|
||||
*/
|
||||
public function offsetUnset($offset): void
|
||||
{
|
||||
unset($this->collection[$offset]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a node at the given offset, or null
|
||||
*
|
||||
* @param mixed $offset
|
||||
* @return mixed
|
||||
*/
|
||||
public function offsetGet($offset)
|
||||
{
|
||||
return isset($this->collection[$offset]) ? $this->collection[$offset] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns this collection as an array.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return $this->collection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Similar to jQuery "each" method. Calls the callback with each
|
||||
* Node in this collection.
|
||||
*
|
||||
* @param callable $callback
|
||||
*/
|
||||
public function each(callable $callback)
|
||||
{
|
||||
foreach ($this->collection as $key => $value) {
|
||||
$callback($value, $key);
|
||||
}
|
||||
}
|
||||
}
|
||||
+207
@@ -0,0 +1,207 @@
|
||||
<?php
|
||||
namespace PHPHtmlParser\Dom;
|
||||
|
||||
use PHPHtmlParser\Exceptions\UnknownChildTypeException;
|
||||
use PHPHtmlParser\Exceptions\ChildNotFoundException;
|
||||
|
||||
/**
|
||||
* Class HtmlNode
|
||||
*
|
||||
* @package PHPHtmlParser\Dom
|
||||
*/
|
||||
class HtmlNode extends InnerNode
|
||||
{
|
||||
|
||||
/**
|
||||
* Remembers what the innerHtml was if it was scanned previously.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $innerHtml = null;
|
||||
|
||||
/**
|
||||
* Remembers what the outerHtml was if it was scanned previously.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $outerHtml = null;
|
||||
|
||||
/**
|
||||
* Remembers what the text was if it was scanned previously.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $text = null;
|
||||
|
||||
/**
|
||||
* Remembers what the text was when we looked into all our
|
||||
* children nodes.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $textWithChildren = null;
|
||||
|
||||
/**
|
||||
* Sets up the tag of this node.
|
||||
*
|
||||
* @param string|Tag $tag
|
||||
*/
|
||||
public function __construct($tag)
|
||||
{
|
||||
if ( ! $tag instanceof Tag) {
|
||||
$tag = new Tag($tag);
|
||||
}
|
||||
$this->tag = $tag;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the inner html of this node.
|
||||
*
|
||||
* @return string
|
||||
* @throws UnknownChildTypeException
|
||||
*/
|
||||
public function innerHtml(): string
|
||||
{
|
||||
if ( ! $this->hasChildren()) {
|
||||
// no children
|
||||
return '';
|
||||
}
|
||||
|
||||
if ( ! is_null($this->innerHtml)) {
|
||||
// we already know the result.
|
||||
return $this->innerHtml;
|
||||
}
|
||||
|
||||
$child = $this->firstChild();
|
||||
$string = '';
|
||||
|
||||
// continue to loop until we are out of children
|
||||
while ( ! is_null($child)) {
|
||||
if ($child instanceof TextNode) {
|
||||
$string .= $child->text();
|
||||
} elseif ($child instanceof HtmlNode) {
|
||||
$string .= $child->outerHtml();
|
||||
} else {
|
||||
throw new UnknownChildTypeException('Unknown child type "'.get_class($child).'" found in node');
|
||||
}
|
||||
|
||||
try {
|
||||
$child = $this->nextChild($child->id());
|
||||
} catch (ChildNotFoundException $e) {
|
||||
// no more children
|
||||
$child = null;
|
||||
}
|
||||
}
|
||||
|
||||
// remember the results
|
||||
$this->innerHtml = $string;
|
||||
|
||||
return $string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the html of this node, including it's own
|
||||
* tag.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function outerHtml(): string
|
||||
{
|
||||
// special handling for root
|
||||
if ($this->tag->name() == 'root') {
|
||||
return $this->innerHtml();
|
||||
}
|
||||
|
||||
if ( ! is_null($this->outerHtml)) {
|
||||
// we already know the results.
|
||||
return $this->outerHtml;
|
||||
}
|
||||
|
||||
$return = $this->tag->makeOpeningTag();
|
||||
if ($this->tag->isSelfClosing()) {
|
||||
// ignore any children... there should not be any though
|
||||
return $return;
|
||||
}
|
||||
|
||||
// get the inner html
|
||||
$return .= $this->innerHtml();
|
||||
|
||||
// add closing tag
|
||||
$return .= $this->tag->makeClosingTag();
|
||||
|
||||
// remember the results
|
||||
$this->outerHtml = $return;
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the text of this node (if there is any text). Or get all the text
|
||||
* in this node, including children.
|
||||
*
|
||||
* @param bool $lookInChildren
|
||||
* @return string
|
||||
*/
|
||||
public function text(bool $lookInChildren = false): string
|
||||
{
|
||||
if ($lookInChildren) {
|
||||
if ( ! is_null($this->textWithChildren)) {
|
||||
// we already know the results.
|
||||
return $this->textWithChildren;
|
||||
}
|
||||
} elseif ( ! is_null($this->text)) {
|
||||
// we already know the results.
|
||||
return $this->text;
|
||||
}
|
||||
|
||||
// find out if this node has any text children
|
||||
$text = '';
|
||||
foreach ($this->children as $child) {
|
||||
/** @var AbstractNode $node */
|
||||
$node = $child['node'];
|
||||
if ($node instanceof TextNode) {
|
||||
$text .= $child['node']->text;
|
||||
} elseif ($lookInChildren &&
|
||||
$node instanceof HtmlNode
|
||||
) {
|
||||
$text .= $node->text($lookInChildren);
|
||||
}
|
||||
}
|
||||
|
||||
// remember our result
|
||||
if ($lookInChildren) {
|
||||
$this->textWithChildren = $text;
|
||||
} else {
|
||||
$this->text = $text;
|
||||
}
|
||||
|
||||
return $text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Call this when something in the node tree has changed. Like a child has been added
|
||||
* or a parent has been changed.
|
||||
*/
|
||||
protected function clear(): void
|
||||
{
|
||||
$this->innerHtml = null;
|
||||
$this->outerHtml = null;
|
||||
$this->text = null;
|
||||
$this->textWithChildren = null;
|
||||
|
||||
if (is_null($this->parent) === false) {
|
||||
$this->parent->clear();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all children of this html node.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function getIteratorArray(): array
|
||||
{
|
||||
return $this->getChildren();
|
||||
}
|
||||
}
|
||||
+442
@@ -0,0 +1,442 @@
|
||||
<?php
|
||||
namespace PHPHtmlParser\Dom;
|
||||
|
||||
use PHPHtmlParser\Exceptions\ChildNotFoundException;
|
||||
use PHPHtmlParser\Exceptions\CircularException;
|
||||
use stringEncode\Encode;
|
||||
|
||||
/**
|
||||
* Inner node of the html tree, might have children.
|
||||
*
|
||||
* @package PHPHtmlParser\Dom
|
||||
*/
|
||||
abstract class InnerNode extends ArrayNode
|
||||
{
|
||||
|
||||
/**
|
||||
* An array of all the children.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $children = [];
|
||||
|
||||
/**
|
||||
* Sets the encoding class to this node and propagates it
|
||||
* to all its children.
|
||||
*
|
||||
* @param Encode $encode
|
||||
* @return void
|
||||
*/
|
||||
public function propagateEncoding(Encode $encode): void
|
||||
{
|
||||
$this->encode = $encode;
|
||||
$this->tag->setEncoding($encode);
|
||||
// check children
|
||||
foreach ($this->children as $id => $child) {
|
||||
/** @var AbstractNode $node */
|
||||
$node = $child['node'];
|
||||
$node->propagateEncoding($encode);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this node has children.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasChildren(): bool
|
||||
{
|
||||
return ! empty($this->children);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the child by id.
|
||||
*
|
||||
* @param int $id
|
||||
* @return AbstractNode
|
||||
* @throws ChildNotFoundException
|
||||
*/
|
||||
public function getChild(int $id): AbstractNode
|
||||
{
|
||||
if ( ! isset($this->children[$id])) {
|
||||
throw new ChildNotFoundException("Child '$id' not found in this node.");
|
||||
}
|
||||
|
||||
return $this->children[$id]['node'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new array of child nodes
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getChildren(): array
|
||||
{
|
||||
$nodes = [];
|
||||
try {
|
||||
$child = $this->firstChild();
|
||||
do {
|
||||
$nodes[] = $child;
|
||||
$child = $this->nextChild($child->id());
|
||||
} while ( ! is_null($child));
|
||||
} catch (ChildNotFoundException $e) {
|
||||
// we are done looking for children
|
||||
}
|
||||
|
||||
return $nodes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Counts children
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function countChildren(): int
|
||||
{
|
||||
return count($this->children);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a child node to this node and returns the id of the child for this
|
||||
* parent.
|
||||
*
|
||||
* @param AbstractNode $child
|
||||
* @param Int $before
|
||||
* @return bool
|
||||
* @throws CircularException
|
||||
*/
|
||||
public function addChild(AbstractNode $child, int $before = -1): bool
|
||||
{
|
||||
$key = null;
|
||||
|
||||
// check integrity
|
||||
if ($this->isAncestor($child->id())) {
|
||||
throw new CircularException('Can not add child. It is my ancestor.');
|
||||
}
|
||||
|
||||
// check if child is itself
|
||||
if ($child->id() == $this->id) {
|
||||
throw new CircularException('Can not set itself as a child.');
|
||||
}
|
||||
|
||||
$next = null;
|
||||
|
||||
if ($this->hasChildren()) {
|
||||
if (isset($this->children[$child->id()])) {
|
||||
// we already have this child
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($before >= 0) {
|
||||
if (!isset($this->children[$before])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$key = $this->children[$before]['prev'];
|
||||
|
||||
if($key){
|
||||
$this->children[$key]['next'] = $child->id();
|
||||
}
|
||||
|
||||
$this->children[$before]['prev'] = $child->id();
|
||||
$next = $before;
|
||||
} else {
|
||||
$sibling = $this->lastChild();
|
||||
$key = $sibling->id();
|
||||
|
||||
$this->children[$key]['next'] = $child->id();
|
||||
}
|
||||
}
|
||||
|
||||
$keys = array_keys($this->children);
|
||||
|
||||
$insert = [
|
||||
'node' => $child,
|
||||
'next' => $next,
|
||||
'prev' => $key,
|
||||
];
|
||||
|
||||
$index = $key ? (array_search($key, $keys, true) + 1) : 0;
|
||||
array_splice($keys, $index, 0, $child->id());
|
||||
|
||||
$children = array_values($this->children);
|
||||
array_splice($children, $index, 0, [$insert]);
|
||||
|
||||
// add the child
|
||||
$this->children = array_combine($keys, $children);
|
||||
|
||||
// tell child I am the new parent
|
||||
$child->setParent($this);
|
||||
|
||||
//clear any cache
|
||||
$this->clear();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert element before child with provided id
|
||||
*
|
||||
* @param AbstractNode $child
|
||||
* @param int $id
|
||||
* @return bool
|
||||
*/
|
||||
public function insertBefore(AbstractNode $child, int $id): bool
|
||||
{
|
||||
return $this->addChild($child, $id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert element before after with provided id
|
||||
*
|
||||
* @param AbstractNode $child
|
||||
* @param int $id
|
||||
* @return bool
|
||||
*/
|
||||
public function insertAfter(AbstractNode $child, int $id): bool
|
||||
{
|
||||
if (!isset($this->children[$id])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->children[$id]['next']) {
|
||||
return $this->addChild($child, $this->children[$id]['next']);
|
||||
}
|
||||
|
||||
// clear cache
|
||||
$this->clear();
|
||||
|
||||
return $this->addChild($child);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the child by id.
|
||||
*
|
||||
* @param int $id
|
||||
* @return InnerNode
|
||||
* @chainable
|
||||
*/
|
||||
public function removeChild(int $id): InnerNode
|
||||
{
|
||||
if ( ! isset($this->children[$id])) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
// handle moving next and previous assignments.
|
||||
$next = $this->children[$id]['next'];
|
||||
$prev = $this->children[$id]['prev'];
|
||||
if ( ! is_null($next)) {
|
||||
$this->children[$next]['prev'] = $prev;
|
||||
}
|
||||
if ( ! is_null($prev)) {
|
||||
$this->children[$prev]['next'] = $next;
|
||||
}
|
||||
|
||||
// remove the child
|
||||
unset($this->children[$id]);
|
||||
|
||||
//clear any cache
|
||||
$this->clear();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if has next Child
|
||||
*
|
||||
* @param int $id
|
||||
* @return mixed
|
||||
*/
|
||||
public function hasNextChild(int $id)
|
||||
{
|
||||
$child= $this->getChild($id);
|
||||
return $this->children[$child->id()]['next'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to get the next child.
|
||||
*
|
||||
* @param int $id
|
||||
* @return AbstractNode
|
||||
* @uses $this->getChild()
|
||||
* @throws ChildNotFoundException
|
||||
*/
|
||||
public function nextChild(int $id): AbstractNode
|
||||
{
|
||||
$child = $this->getChild($id);
|
||||
$next = $this->children[$child->id()]['next'];
|
||||
if (is_null($next)) {
|
||||
throw new ChildNotFoundException("Child '$id' next not found in this node.");
|
||||
}
|
||||
|
||||
return $this->getChild($next);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to get the previous child.
|
||||
*
|
||||
* @param int $id
|
||||
* @return AbstractNode
|
||||
* @uses $this->getChild()
|
||||
* @throws ChildNotFoundException
|
||||
*/
|
||||
public function previousChild(int $id): AbstractNode
|
||||
{
|
||||
$child = $this->getchild($id);
|
||||
$next = $this->children[$child->id()]['prev'];
|
||||
if (is_null($next)) {
|
||||
throw new ChildNotFoundException("Child '$id' previous not found in this node.");
|
||||
}
|
||||
|
||||
return $this->getChild($next);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given node id is a child of the
|
||||
* current node.
|
||||
*
|
||||
* @param int $id
|
||||
* @return bool
|
||||
*/
|
||||
public function isChild(int $id): bool
|
||||
{
|
||||
foreach ($this->children as $childId => $child) {
|
||||
if ($id == $childId) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the child with id $childId and replace it with the new child
|
||||
* $newChild.
|
||||
*
|
||||
* @param int $childId
|
||||
* @param AbstractNode $newChild
|
||||
* @throws ChildNotFoundException
|
||||
* @return void
|
||||
*/
|
||||
public function replaceChild(int $childId, AbstractNode $newChild): void
|
||||
{
|
||||
$oldChild = $this->children[$childId];
|
||||
|
||||
$newChild->prev = $oldChild['prev'];
|
||||
$newChild->next = $oldChild['next'];
|
||||
|
||||
$keys = array_keys($this->children);
|
||||
$index = array_search($childId, $keys, true);
|
||||
$keys[$index] = $newChild->id();
|
||||
$this->children = array_combine($keys, $this->children);
|
||||
$this->children[$newChild->id()] = array(
|
||||
'prev' => $oldChild['prev'],
|
||||
'node' => $newChild,
|
||||
'next' => $oldChild['next']
|
||||
);
|
||||
|
||||
// chnge previous child id to new child
|
||||
if ($oldChild['prev'] && isset($this->children[$newChild->prev])) {
|
||||
$this->children[$oldChild['prev']]['next'] = $newChild->id();
|
||||
}
|
||||
|
||||
// change next child id to new child
|
||||
if ($oldChild['next'] && isset($this->children[$newChild->next])) {
|
||||
$this->children[$oldChild['next']]['prev'] = $newChild->id();
|
||||
}
|
||||
|
||||
// remove old child
|
||||
unset($this->children[$childId]);
|
||||
|
||||
// clean out cache
|
||||
$this->clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Shortcut to return the first child.
|
||||
*
|
||||
* @return AbstractNode
|
||||
* @uses $this->getChild()
|
||||
* @throws ChildNotFoundException
|
||||
*/
|
||||
public function firstChild(): AbstractNode
|
||||
{
|
||||
if (count($this->children) == 0) {
|
||||
// no children
|
||||
throw new ChildNotFoundException("No children found in node.");
|
||||
}
|
||||
|
||||
reset($this->children);
|
||||
$key = (int) key($this->children);
|
||||
|
||||
return $this->getChild($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to get the last child.
|
||||
*
|
||||
* @return AbstractNode
|
||||
* @uses $this->getChild()
|
||||
* @throws ChildNotFoundException
|
||||
*/
|
||||
public function lastChild(): AbstractNode
|
||||
{
|
||||
if (count($this->children) == 0) {
|
||||
// no children
|
||||
throw new ChildNotFoundException("No children found in node.");
|
||||
}
|
||||
|
||||
end($this->children);
|
||||
$key = key($this->children);
|
||||
|
||||
return $this->getChild($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given node id is a descendant of the
|
||||
* current node.
|
||||
*
|
||||
* @param int $id
|
||||
* @return bool
|
||||
*/
|
||||
public function isDescendant(int $id): bool
|
||||
{
|
||||
if ($this->isChild($id)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach ($this->children as $childId => $child) {
|
||||
/** @var InnerNode $node */
|
||||
$node = $child['node'];
|
||||
if ($node instanceof InnerNode &&
|
||||
$node->hasChildren() &&
|
||||
$node->isDescendant($id)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the parent node.
|
||||
*
|
||||
* @param InnerNode $parent
|
||||
* @return AbstractNode
|
||||
* @throws CircularException
|
||||
* @chainable
|
||||
*/
|
||||
public function setParent(InnerNode $parent): AbstractNode
|
||||
{
|
||||
// check integrity
|
||||
if ($this->isDescendant($parent->id())) {
|
||||
throw new CircularException('Can not add descendant "'.$parent->id().'" as my parent.');
|
||||
}
|
||||
|
||||
// clear cache
|
||||
$this->clear();
|
||||
|
||||
return parent::setParent($parent);
|
||||
}
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
namespace PHPHtmlParser\Dom;
|
||||
|
||||
|
||||
/**
|
||||
* Class LeafNode
|
||||
*
|
||||
* @package PHPHtmlParser
|
||||
*/
|
||||
abstract class LeafNode extends AbstractNode
|
||||
{
|
||||
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
namespace PHPHtmlParser\Dom;
|
||||
|
||||
/**
|
||||
* This mock object is used solely for testing the abstract
|
||||
* class Node with out any potential side effects caused
|
||||
* by testing a supper class of Node.
|
||||
*
|
||||
* This object is not to be used for any other reason.
|
||||
*/
|
||||
class MockNode extends InnerNode
|
||||
{
|
||||
|
||||
/**
|
||||
* Mock of innner html.
|
||||
*/
|
||||
public function innerHtml(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Mock of outer html.
|
||||
*/
|
||||
public function outerHtml(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Mock of text.
|
||||
*/
|
||||
public function text(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear content of this node
|
||||
*/
|
||||
protected function clear(): void
|
||||
{
|
||||
$this->innerHtml = null;
|
||||
$this->outerHtml = null;
|
||||
$this->text = null;
|
||||
if (is_null($this->parent) === false) {
|
||||
$this->parent->clear();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all children of this html node.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function getIteratorArray(): array
|
||||
{
|
||||
return $this->getChildren();
|
||||
}
|
||||
}
|
||||
+351
@@ -0,0 +1,351 @@
|
||||
<?php
|
||||
namespace PHPHtmlParser\Dom;
|
||||
|
||||
use PHPHtmlParser\Dom;
|
||||
use stringEncode\Encode;
|
||||
|
||||
/**
|
||||
* Class Tag
|
||||
*
|
||||
* @package PHPHtmlParser\Dom
|
||||
*/
|
||||
class Tag
|
||||
{
|
||||
|
||||
/**
|
||||
* The name of the tag.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $name;
|
||||
|
||||
/**
|
||||
* The attributes of the tag.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $attr = [];
|
||||
|
||||
/**
|
||||
* Is this tag self closing.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $selfClosing = false;
|
||||
|
||||
/**
|
||||
* If self-closing, will this use a trailing slash. />
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $trailingSlash = true;
|
||||
|
||||
/**
|
||||
* Tag noise
|
||||
*/
|
||||
protected $noise = '';
|
||||
|
||||
/**
|
||||
* The encoding class to... encode the tags
|
||||
*
|
||||
* @var mixed
|
||||
*/
|
||||
protected $encode = null;
|
||||
|
||||
/**
|
||||
* Sets up the tag with a name.
|
||||
*
|
||||
* @param $name
|
||||
*/
|
||||
public function __construct(string $name)
|
||||
{
|
||||
$this->name = $name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Magic method to get any of the attributes.
|
||||
*
|
||||
* @param string $key
|
||||
* @return mixed
|
||||
*/
|
||||
public function __get($key)
|
||||
{
|
||||
return $this->getAttribute($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Magic method to set any attribute.
|
||||
*
|
||||
* @param string $key
|
||||
* @param mixed $value
|
||||
*/
|
||||
public function __set($key, $value)
|
||||
{
|
||||
$this->setAttribute($key, $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of this tag.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function name(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the tag to be self closing.
|
||||
*
|
||||
* @return Tag
|
||||
* @chainable
|
||||
*/
|
||||
public function selfClosing(): Tag
|
||||
{
|
||||
$this->selfClosing = true;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sets the tag to not use a trailing slash.
|
||||
*
|
||||
* @return Tag
|
||||
* @chainable
|
||||
*/
|
||||
public function noTrailingSlash(): Tag
|
||||
{
|
||||
$this->trailingSlash = false;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the tag is self closing.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isSelfClosing(): bool
|
||||
{
|
||||
return $this->selfClosing;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the encoding type to be used.
|
||||
*
|
||||
* @param Encode $encode
|
||||
* @return void
|
||||
*/
|
||||
public function setEncoding(Encode $encode): void
|
||||
{
|
||||
$this->encode = $encode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the noise for this tag (if any)
|
||||
*
|
||||
* @param string $noise
|
||||
* @return Tag
|
||||
* @chainable
|
||||
*/
|
||||
public function noise(string $noise): Tag
|
||||
{
|
||||
$this->noise = $noise;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set an attribute for this tag.
|
||||
*
|
||||
* @param string $key
|
||||
* @param string|array $value
|
||||
* @return Tag
|
||||
* @chainable
|
||||
*/
|
||||
public function setAttribute(string $key, $value): Tag
|
||||
{
|
||||
$key = strtolower($key);
|
||||
if ( ! is_array($value)) {
|
||||
$value = [
|
||||
'value' => $value,
|
||||
'doubleQuote' => true,
|
||||
];
|
||||
}
|
||||
$this->attr[$key] = $value;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set inline style attribute value.
|
||||
*
|
||||
* @param mixed $attr_key
|
||||
* @param mixed $attr_value
|
||||
*/
|
||||
public function setStyleAttributeValue($attr_key, $attr_value): void
|
||||
{
|
||||
|
||||
$style_array = $this->getStyleAttributeArray();
|
||||
$style_array[$attr_key] = $attr_value;
|
||||
|
||||
$style_string = '';
|
||||
foreach ($style_array as $key => $value) {
|
||||
$style_string .= $key . ':' . $value . ';';
|
||||
}
|
||||
|
||||
$this->setAttribute('style', $style_string);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get style attribute in array
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getStyleAttributeArray(): array
|
||||
{
|
||||
$value = $this->getAttribute('style')['value'];
|
||||
|
||||
if ($value === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$value = explode(';', substr(trim($value), 0, -1));
|
||||
$result = [];
|
||||
foreach ($value as $attr) {
|
||||
$attr = explode(':', $attr);
|
||||
$result[$attr[0]] = $attr[1];
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Removes an attribute from this tag.
|
||||
*
|
||||
* @param mixed $key
|
||||
* @return void
|
||||
*/
|
||||
public function removeAttribute($key)
|
||||
{
|
||||
$key = strtolower($key);
|
||||
unset($this->attr[$key]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all attributes on this tag.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function removeAllAttributes()
|
||||
{
|
||||
$this->attr = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the attributes for this tag
|
||||
*
|
||||
* @param array $attr
|
||||
* @return $this
|
||||
*/
|
||||
public function setAttributes(array $attr)
|
||||
{
|
||||
foreach ($attr as $key => $value) {
|
||||
$this->setAttribute($key, $value);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all attributes of this tag.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getAttributes()
|
||||
{
|
||||
$return = [];
|
||||
foreach ($this->attr as $attr => $info) {
|
||||
$return[$attr] = $this->getAttribute($attr);
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an attribute by the key
|
||||
*
|
||||
* @param string $key
|
||||
* @return mixed
|
||||
*/
|
||||
public function getAttribute(string $key)
|
||||
{
|
||||
if ( ! isset($this->attr[$key])) {
|
||||
return null;
|
||||
}
|
||||
$value = $this->attr[$key]['value'];
|
||||
if (is_string($value) && ! is_null($this->encode)) {
|
||||
// convert charset
|
||||
$this->attr[$key]['value'] = $this->encode->convert($value);
|
||||
}
|
||||
|
||||
return $this->attr[$key];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns TRUE if node has attribute
|
||||
*
|
||||
* @param string $key
|
||||
* @return bool
|
||||
*/
|
||||
public function hasAttribute(string $key)
|
||||
{
|
||||
return isset($this->attr[$key]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the opening tag for this object.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function makeOpeningTag()
|
||||
{
|
||||
$return = '<'.$this->name;
|
||||
|
||||
// add the attributes
|
||||
foreach ($this->attr as $key => $info) {
|
||||
$info = $this->getAttribute($key);
|
||||
$val = $info['value'];
|
||||
if (is_null($val)) {
|
||||
$return .= ' '.$key;
|
||||
} elseif ($info['doubleQuote']) {
|
||||
$return .= ' '.$key.'="'.$val.'"';
|
||||
} else {
|
||||
$return .= ' '.$key.'=\''.$val.'\'';
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->selfClosing && $this->trailingSlash) {
|
||||
return $return.' />';
|
||||
} else {
|
||||
return $return.'>';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the closing tag for this object.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function makeClosingTag()
|
||||
{
|
||||
if ($this->selfClosing) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return '</'.$this->name.'>';
|
||||
}
|
||||
}
|
||||
+136
@@ -0,0 +1,136 @@
|
||||
<?php
|
||||
namespace PHPHtmlParser\Dom;
|
||||
|
||||
/**
|
||||
* Class TextNode
|
||||
*
|
||||
* @package PHPHtmlParser\Dom
|
||||
*/
|
||||
class TextNode extends LeafNode
|
||||
{
|
||||
|
||||
/**
|
||||
* This is a text node.
|
||||
*
|
||||
* @var Tag
|
||||
*/
|
||||
protected $tag;
|
||||
|
||||
/**
|
||||
* This is the text in this node.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $text;
|
||||
|
||||
/**
|
||||
* This is the converted version of the text.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $convertedText = null;
|
||||
|
||||
/**
|
||||
* Sets the text for this node.
|
||||
*
|
||||
* @param string $text
|
||||
* @param bool $removeDoubleSpace
|
||||
*/
|
||||
public function __construct(string $text, $removeDoubleSpace = true)
|
||||
{
|
||||
if ($removeDoubleSpace) {
|
||||
// remove double spaces
|
||||
$text = mb_ereg_replace('\s+', ' ', $text);
|
||||
}
|
||||
|
||||
// restore line breaks
|
||||
$text = str_replace(' ', "\n", $text);
|
||||
|
||||
$this->text = $text;
|
||||
$this->tag = new Tag('text');
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the text of this node.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function text(): string
|
||||
{
|
||||
// convert charset
|
||||
if ( ! is_null($this->encode)) {
|
||||
if ( ! is_null($this->convertedText)) {
|
||||
// we already know the converted value
|
||||
return $this->convertedText;
|
||||
}
|
||||
$text = $this->encode->convert($this->text);
|
||||
|
||||
// remember the conversion
|
||||
$this->convertedText = $text;
|
||||
|
||||
return $text;
|
||||
} else {
|
||||
return $this->text;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the text for this node.
|
||||
*
|
||||
* @var string $text
|
||||
* @return void
|
||||
*/
|
||||
public function setText(string $text): void
|
||||
{
|
||||
$this->text = $text;
|
||||
|
||||
if ( ! is_null($this->encode)) {
|
||||
$text = $this->encode->convert($text);
|
||||
|
||||
// remember the conversion
|
||||
$this->convertedText = $text;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This node has no html, just return the text.
|
||||
*
|
||||
* @return string
|
||||
* @uses $this->text()
|
||||
*/
|
||||
public function innerHtml(): string
|
||||
{
|
||||
return $this->text();
|
||||
}
|
||||
|
||||
/**
|
||||
* This node has no html, just return the text.
|
||||
*
|
||||
* @return string
|
||||
* @uses $this->text()
|
||||
*/
|
||||
public function outerHtml(): string
|
||||
{
|
||||
return $this->text();
|
||||
}
|
||||
|
||||
/**
|
||||
* Call this when something in the node tree has changed. Like a child has been added
|
||||
* or a parent has been changed.
|
||||
*/
|
||||
protected function clear(): void
|
||||
{
|
||||
$this->convertedText = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the current node is a text node.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isTextNode(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
namespace PHPHtmlParser\Exceptions;
|
||||
|
||||
/**
|
||||
* Class ChildNotFoundException
|
||||
*
|
||||
* @package PHPHtmlParser\Exceptions
|
||||
*/
|
||||
final class ChildNotFoundException extends \Exception
|
||||
{
|
||||
}
|
||||
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
namespace PHPHtmlParser\Exceptions;
|
||||
|
||||
/**
|
||||
* Class CircularException
|
||||
*
|
||||
* @package PHPHtmlParser\Exceptions
|
||||
*/
|
||||
final class CircularException extends \Exception
|
||||
{
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
namespace PHPHtmlParser\Exceptions;
|
||||
|
||||
/**
|
||||
* Class CurlException
|
||||
*
|
||||
* @package PHPHtmlParser\Exceptions
|
||||
*/
|
||||
class CurlException extends \Exception
|
||||
{
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
namespace PHPHtmlParser\Exceptions;
|
||||
|
||||
/**
|
||||
* Class EmptyCollectionException
|
||||
*
|
||||
* @package PHPHtmlParser\Exceptions
|
||||
*/
|
||||
final class EmptyCollectionException extends \Exception
|
||||
{
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
namespace PHPHtmlParser\Exceptions;
|
||||
|
||||
/**
|
||||
* Class NotLoadedException
|
||||
*
|
||||
* @package PHPHtmlParser\Exceptions
|
||||
*/
|
||||
final class NotLoadedException extends \Exception
|
||||
{
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
namespace PHPHtmlParser\Exceptions;
|
||||
|
||||
/**
|
||||
* Class ParentNotFoundException
|
||||
*
|
||||
* @package PHPHtmlParser\Exceptions
|
||||
*/
|
||||
final class ParentNotFoundException extends \Exception
|
||||
{
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
namespace PHPHtmlParser\Exceptions;
|
||||
|
||||
/**
|
||||
* Class StrictException
|
||||
*
|
||||
* @package PHPHtmlParser\Exceptions
|
||||
*/
|
||||
final class StrictException extends \Exception
|
||||
{
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
namespace PHPHtmlParser\Exceptions;
|
||||
|
||||
/**
|
||||
* Class UnknownChildTypeException
|
||||
*
|
||||
* @package PHPHtmlParser\Exceptions
|
||||
*/
|
||||
final class UnknownChildTypeException extends \Exception
|
||||
{
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace PHPHtmlParser;
|
||||
|
||||
use PHPHtmlParser\Dom\AbstractNode;
|
||||
use PHPHtmlParser\Dom\InnerNode;
|
||||
|
||||
class Finder
|
||||
{
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* Finder constructor.
|
||||
* @param $id
|
||||
*/
|
||||
public function __construct($id)
|
||||
{
|
||||
$this->id = $id;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Find node in tree by id
|
||||
*
|
||||
* @param AbstractNode $node
|
||||
* @return bool|AbstractNode
|
||||
*/
|
||||
public function find(AbstractNode $node)
|
||||
{
|
||||
if (!$node->id() && $node instanceof InnerNode) {
|
||||
return $this->find($node->firstChild());
|
||||
}
|
||||
|
||||
if ($node->id() == $this->id) {
|
||||
return $node;
|
||||
}
|
||||
|
||||
if ($node->hasNextSibling()) {
|
||||
$nextSibling = $node->nextSibling();
|
||||
if ($nextSibling->id() == $this->id) {
|
||||
return $nextSibling;
|
||||
}
|
||||
if ($nextSibling->id() > $this->id) {
|
||||
return $this->find($node->firstChild());
|
||||
}
|
||||
if ($nextSibling->id() < $this->id) {
|
||||
return $this->find($nextSibling);
|
||||
}
|
||||
} else if (!$node->isTextNode()) {
|
||||
return $this->find($node->firstChild());
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
namespace PHPHtmlParser;
|
||||
|
||||
/**
|
||||
* Class Options
|
||||
*
|
||||
* @package PHPHtmlParser
|
||||
* @property bool whitespaceTextNode
|
||||
* @property bool strict
|
||||
* @property string|null enforceEncoding
|
||||
* @property bool cleanupInput
|
||||
* @property bool removeScripts
|
||||
* @property bool removeStyles
|
||||
* @property bool preserveLineBreaks
|
||||
* @property bool removeDoubleSpace
|
||||
*/
|
||||
class Options
|
||||
{
|
||||
|
||||
/**
|
||||
* The default options array
|
||||
*
|
||||
* @param array
|
||||
*/
|
||||
protected $defaults = [
|
||||
'whitespaceTextNode' => true,
|
||||
'strict' => false,
|
||||
'enforceEncoding' => null,
|
||||
'cleanupInput' => true,
|
||||
'removeScripts' => true,
|
||||
'removeStyles' => true,
|
||||
'preserveLineBreaks' => false,
|
||||
'removeDoubleSpace' => true,
|
||||
];
|
||||
|
||||
/**
|
||||
* The list of all current options set.
|
||||
*
|
||||
* @param array
|
||||
*/
|
||||
protected $options = [];
|
||||
|
||||
/**
|
||||
* Sets the default options in the options array
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->options = $this->defaults;
|
||||
}
|
||||
|
||||
/**
|
||||
* A magic get to call the get() method.
|
||||
*
|
||||
* @param string $key
|
||||
* @return mixed
|
||||
* @uses $this->get()
|
||||
*/
|
||||
public function __get($key)
|
||||
{
|
||||
return $this->get($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a new options param to override the current option array.
|
||||
*
|
||||
* @param array $options
|
||||
* @return Options
|
||||
* @chainable
|
||||
*/
|
||||
public function setOptions(array $options): Options
|
||||
{
|
||||
foreach ($options as $key => $option) {
|
||||
$this->options[$key] = $option;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the value associated to the key, or null if the key is not
|
||||
* found.
|
||||
*
|
||||
* @param string
|
||||
* @return mixed
|
||||
*/
|
||||
public function get(string $key)
|
||||
{
|
||||
if (isset($this->options[$key])) {
|
||||
return $this->options[$key];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
namespace PHPHtmlParser\Selector;
|
||||
|
||||
/**
|
||||
* This is the parser for the selctor.
|
||||
*
|
||||
*
|
||||
*/
|
||||
class Parser implements ParserInterface
|
||||
{
|
||||
|
||||
/**
|
||||
* Pattern of CSS selectors, modified from 'mootools'
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $pattern = "/([\w\-:\*>]*)(?:\#([\w\-]+)|\.([\w\-]+))?(?:\[@?(!?[\w\-:]+)(?:([!*^$]?=)[\"']?(.*?)[\"']?)?\])?([\/, ]+)/is";
|
||||
|
||||
/**
|
||||
* Parses the selector string
|
||||
*
|
||||
* @param string $selector
|
||||
*/
|
||||
public function parseSelectorString(string $selector): array
|
||||
{
|
||||
$selectors = [];
|
||||
|
||||
$matches = [];
|
||||
preg_match_all($this->pattern, trim($selector).' ', $matches, PREG_SET_ORDER);
|
||||
|
||||
// skip tbody
|
||||
$result = [];
|
||||
foreach ($matches as $match) {
|
||||
// default values
|
||||
$tag = strtolower(trim($match[1]));
|
||||
$operator = '=';
|
||||
$key = null;
|
||||
$value = null;
|
||||
$noKey = false;
|
||||
$alterNext = false;
|
||||
|
||||
// check for elements that alter the behavior of the next element
|
||||
if ($tag == '>') {
|
||||
$alterNext = true;
|
||||
}
|
||||
|
||||
// check for id selector
|
||||
if ( ! empty($match[2])) {
|
||||
$key = 'id';
|
||||
$value = $match[2];
|
||||
}
|
||||
|
||||
// check for class selector
|
||||
if ( ! empty($match[3])) {
|
||||
$key = 'class';
|
||||
$value = $match[3];
|
||||
}
|
||||
|
||||
// and final attribute selector
|
||||
if ( ! empty($match[4])) {
|
||||
$key = strtolower($match[4]);
|
||||
}
|
||||
if ( ! empty($match[5])) {
|
||||
$operator = $match[5];
|
||||
}
|
||||
if ( ! empty($match[6])) {
|
||||
$value = $match[6];
|
||||
}
|
||||
|
||||
// check for elements that do not have a specified attribute
|
||||
if (isset($key[0]) && $key[0] == '!') {
|
||||
$key = substr($key, 1);
|
||||
$noKey = true;
|
||||
}
|
||||
|
||||
$result[] = [
|
||||
'tag' => $tag,
|
||||
'key' => $key,
|
||||
'value' => $value,
|
||||
'operator' => $operator,
|
||||
'noKey' => $noKey,
|
||||
'alterNext' => $alterNext,
|
||||
];
|
||||
if (trim($match[7]) == ',') {
|
||||
$selectors[] = $result;
|
||||
$result = [];
|
||||
}
|
||||
}
|
||||
|
||||
// save last results
|
||||
if (count($result) > 0) {
|
||||
$selectors[] = $result;
|
||||
}
|
||||
|
||||
return $selectors;
|
||||
}
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
namespace PHPHtmlParser\Selector;
|
||||
|
||||
interface ParserInterface
|
||||
{
|
||||
public function parseSelectorString(string $selector): array;
|
||||
}
|
||||
+338
@@ -0,0 +1,338 @@
|
||||
<?php
|
||||
namespace PHPHtmlParser\Selector;
|
||||
|
||||
use PHPHtmlParser\Dom\AbstractNode;
|
||||
use PHPHtmlParser\Dom\Collection;
|
||||
use PHPHtmlParser\Dom\InnerNode;
|
||||
use PHPHtmlParser\Dom\LeafNode;
|
||||
use PHPHtmlParser\Exceptions\ChildNotFoundException;
|
||||
|
||||
/**
|
||||
* Class Selector
|
||||
*
|
||||
* @package PHPHtmlParser
|
||||
*/
|
||||
class Selector
|
||||
{
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $selectors = [];
|
||||
|
||||
/**
|
||||
* Constructs with the selector string
|
||||
*
|
||||
* @param string $selector
|
||||
*/
|
||||
public function __construct(string $selector, ParserInterface $parser)
|
||||
{
|
||||
$this->selectors = $parser->parseSelectorString($selector);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the selectors that where found in __construct
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getSelectors()
|
||||
{
|
||||
return $this->selectors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to find the selectors starting from the given
|
||||
* node object.
|
||||
*
|
||||
* @param AbstractNode $node
|
||||
* @return Collection
|
||||
*/
|
||||
public function find(AbstractNode $node): Collection
|
||||
{
|
||||
$results = new Collection;
|
||||
foreach ($this->selectors as $selector) {
|
||||
$nodes = [$node];
|
||||
if (count($selector) == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$options = [];
|
||||
foreach ($selector as $rule) {
|
||||
if ($rule['alterNext']) {
|
||||
$options[] = $this->alterNext($rule);
|
||||
continue;
|
||||
}
|
||||
$nodes = $this->seek($nodes, $rule, $options);
|
||||
// clear the options
|
||||
$options = [];
|
||||
}
|
||||
|
||||
// this is the final set of nodes
|
||||
foreach ($nodes as $result) {
|
||||
$results[] = $result;
|
||||
}
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Attempts to find all children that match the rule
|
||||
* given.
|
||||
*
|
||||
* @param array $nodes
|
||||
* @param array $rule
|
||||
* @param array $options
|
||||
* @return array
|
||||
* @recursive
|
||||
*/
|
||||
protected function seek(array $nodes, array $rule, array $options): array
|
||||
{
|
||||
// XPath index
|
||||
if (array_key_exists('tag', $rule) &&
|
||||
array_key_exists('key', $rule) &&
|
||||
is_numeric($rule['key'])
|
||||
) {
|
||||
$count = 0;
|
||||
/** @var AbstractNode $node */
|
||||
foreach ($nodes as $node) {
|
||||
if ($rule['tag'] == '*' ||
|
||||
$rule['tag'] == $node->getTag()->name()
|
||||
) {
|
||||
++$count;
|
||||
if ($count == $rule['key']) {
|
||||
// found the node we wanted
|
||||
return [$node];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
$options = $this->flattenOptions($options);
|
||||
|
||||
$return = [];
|
||||
/** @var InnerNode $node */
|
||||
foreach ($nodes as $node) {
|
||||
// check if we are a leaf
|
||||
if ($node instanceof LeafNode ||
|
||||
! $node->hasChildren()
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$children = [];
|
||||
$child = $node->firstChild();
|
||||
while ( ! is_null($child)) {
|
||||
// wild card, grab all
|
||||
if ($rule['tag'] == '*' && is_null($rule['key'])) {
|
||||
$return[] = $child;
|
||||
$child = $this->getNextChild($node, $child);
|
||||
continue;
|
||||
}
|
||||
|
||||
$pass = $this->checkTag($rule, $child);
|
||||
if ($pass && ! is_null($rule['key'])) {
|
||||
$pass = $this->checkKey($rule, $child);
|
||||
}
|
||||
if ($pass && ! is_null($rule['key']) &&
|
||||
! is_null($rule['value']) && $rule['value'] != '*'
|
||||
) {
|
||||
$pass = $this->checkComparison($rule, $child);
|
||||
}
|
||||
|
||||
if ($pass) {
|
||||
// it passed all checks
|
||||
$return[] = $child;
|
||||
} else {
|
||||
// this child failed to be matched
|
||||
if ($child instanceof InnerNode &&
|
||||
$child->hasChildren()
|
||||
) {
|
||||
// we still want to check its children
|
||||
$children[] = $child;
|
||||
}
|
||||
}
|
||||
|
||||
$child = $this->getNextChild($node, $child);
|
||||
}
|
||||
|
||||
if (( ! isset($options['checkGrandChildren']) ||
|
||||
$options['checkGrandChildren'])
|
||||
&& count($children) > 0
|
||||
) {
|
||||
// we have children that failed but are not leaves.
|
||||
$matches = $this->seek($children, $rule, $options);
|
||||
foreach ($matches as $match) {
|
||||
$return[] = $match;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to match the given arguments with the given operator.
|
||||
*
|
||||
* @param string $operator
|
||||
* @param string $pattern
|
||||
* @param string $value
|
||||
* @return bool
|
||||
*/
|
||||
protected function match(string $operator, string $pattern, string $value): bool
|
||||
{
|
||||
$value = strtolower($value);
|
||||
$pattern = strtolower($pattern);
|
||||
switch ($operator) {
|
||||
case '=':
|
||||
return $value === $pattern;
|
||||
case '!=':
|
||||
return $value !== $pattern;
|
||||
case '^=':
|
||||
return preg_match('/^'.preg_quote($pattern, '/').'/', $value) == 1;
|
||||
case '$=':
|
||||
return preg_match('/'.preg_quote($pattern, '/').'$/', $value) == 1;
|
||||
case '*=':
|
||||
if ($pattern[0] == '/') {
|
||||
return preg_match($pattern, $value) == 1;
|
||||
}
|
||||
|
||||
return preg_match("/".$pattern."/i", $value) == 1;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to figure out what the alteration will be for
|
||||
* the next element.
|
||||
*
|
||||
* @param array $rule
|
||||
* @return array
|
||||
*/
|
||||
protected function alterNext(array $rule): array
|
||||
{
|
||||
$options = [];
|
||||
if ($rule['tag'] == '>') {
|
||||
$options['checkGrandChildren'] = false;
|
||||
}
|
||||
|
||||
return $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Flattens the option array.
|
||||
*
|
||||
* @param array $optionsArray
|
||||
* @return array
|
||||
*/
|
||||
protected function flattenOptions(array $optionsArray)
|
||||
{
|
||||
$options = [];
|
||||
foreach ($optionsArray as $optionArray) {
|
||||
foreach ($optionArray as $key => $option) {
|
||||
$options[$key] = $option;
|
||||
}
|
||||
}
|
||||
|
||||
return $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the next child or null if no more children.
|
||||
*
|
||||
* @param AbstractNode $node
|
||||
* @param AbstractNode $currentChild
|
||||
* @return AbstractNode|null
|
||||
*/
|
||||
protected function getNextChild(AbstractNode $node, AbstractNode $currentChild)
|
||||
{
|
||||
try {
|
||||
// get next child
|
||||
$child = $node->nextChild($currentChild->id());
|
||||
} catch (ChildNotFoundException $e) {
|
||||
// no more children
|
||||
$child = null;
|
||||
}
|
||||
|
||||
return $child;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks tag condition from rules against node.
|
||||
*
|
||||
* @param array $rule
|
||||
* @param AbstractNode $node
|
||||
* @return bool
|
||||
*/
|
||||
protected function checkTag(array $rule, AbstractNode $node): bool
|
||||
{
|
||||
if ( ! empty($rule['tag']) && $rule['tag'] != $node->getTag()->name() &&
|
||||
$rule['tag'] != '*'
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks key condition from rules against node.
|
||||
*
|
||||
* @param array $rule
|
||||
* @param AbstractNode $node
|
||||
* @return bool
|
||||
*/
|
||||
protected function checkKey(array $rule, AbstractNode $node): bool
|
||||
{
|
||||
if ($rule['noKey']) {
|
||||
if ( ! is_null($node->getAttribute($rule['key']))) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
if ($rule['key'] != 'plaintext' && !$node->hasAttribute($rule['key'])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks comparison condition from rules against node.
|
||||
*
|
||||
* @param array $rule
|
||||
* @param AbstractNode $node
|
||||
* @return bool
|
||||
*/
|
||||
public function checkComparison(array $rule, AbstractNode $node): bool
|
||||
{
|
||||
if ($rule['key'] == 'plaintext') {
|
||||
// plaintext search
|
||||
$nodeValue = $node->text();
|
||||
} else {
|
||||
// normal search
|
||||
$nodeValue = $node->getAttribute($rule['key']);
|
||||
}
|
||||
|
||||
$check = $this->match($rule['operator'], $rule['value'], $nodeValue);
|
||||
|
||||
// handle multiple classes
|
||||
if ( ! $check && $rule['key'] == 'class') {
|
||||
$nodeClasses = explode(' ', $node->getAttribute('class'));
|
||||
foreach ($nodeClasses as $class) {
|
||||
if ( ! empty($class)) {
|
||||
$check = $this->match($rule['operator'], $rule['value'], $class);
|
||||
}
|
||||
if ($check) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $check;
|
||||
}
|
||||
}
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
<?php
|
||||
namespace PHPHtmlParser;
|
||||
|
||||
use PHPHtmlParser\Exceptions\NotLoadedException;
|
||||
|
||||
/**
|
||||
* Class StaticDom
|
||||
*
|
||||
* @package PHPHtmlParser
|
||||
*/
|
||||
final class StaticDom
|
||||
{
|
||||
|
||||
private static $dom = null;
|
||||
|
||||
/**
|
||||
* Attempts to call the given method on the most recent created dom
|
||||
* from bellow.
|
||||
*
|
||||
* @param string $method
|
||||
* @param array $arguments
|
||||
* @throws NotLoadedException
|
||||
* @return mixed
|
||||
*/
|
||||
public static function __callStatic(string $method, array $arguments)
|
||||
{
|
||||
if (self::$dom instanceof Dom) {
|
||||
return call_user_func_array([self::$dom, $method], $arguments);
|
||||
} else {
|
||||
throw new NotLoadedException('The dom is not loaded. Can not call a dom method.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Call this to mount the static facade. The facade allows you to use
|
||||
* this object as a $className.
|
||||
*
|
||||
* @param string $className
|
||||
* @param Dom $dom
|
||||
* @return bool
|
||||
*/
|
||||
public static function mount(string $className = 'Dom', Dom $dom = null): bool
|
||||
{
|
||||
if (class_exists($className)) {
|
||||
return false;
|
||||
}
|
||||
class_alias(__CLASS__, $className);
|
||||
if ($dom instanceof Dom) {
|
||||
self::$dom = $dom;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new dom object and calls load() on the
|
||||
* new object.
|
||||
*
|
||||
* @param string $str
|
||||
* @return Dom
|
||||
*/
|
||||
public static function load(string $str): Dom
|
||||
{
|
||||
$dom = new Dom;
|
||||
self::$dom = $dom;
|
||||
|
||||
return $dom->load($str);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new dom object and calls loadFromFile() on the
|
||||
* new object.
|
||||
*
|
||||
* @param string $file
|
||||
* @return Dom
|
||||
*/
|
||||
public static function loadFromFile(string $file): Dom
|
||||
{
|
||||
$dom = new Dom;
|
||||
self::$dom = $dom;
|
||||
|
||||
return $dom->loadFromFile($file);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new dom object and calls loadFromUrl() on the
|
||||
* new object.
|
||||
*
|
||||
* @param string $url
|
||||
* @param array $options
|
||||
* @param CurlInterface $curl
|
||||
* @return Dom
|
||||
*/
|
||||
public static function loadFromUrl(string $url, array $options = [], CurlInterface $curl = null): Dom
|
||||
{
|
||||
$dom = new Dom;
|
||||
self::$dom = $dom;
|
||||
if (is_null($curl)) {
|
||||
// use the default curl interface
|
||||
$curl = new Curl;
|
||||
}
|
||||
|
||||
return $dom->loadFromUrl($url, $options, $curl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the $dom variable to null.
|
||||
*/
|
||||
public static function unload(): void
|
||||
{
|
||||
self::$dom = null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user