loader ajax

This commit is contained in:
2019-10-06 01:13:05 +02:00
parent 8a48a6ac0f
commit ba59f31741
355 changed files with 11286 additions and 707 deletions
@@ -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
@@ -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
@@ -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
}
@@ -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);
}
}
@@ -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;
}
}
@@ -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;
}
@@ -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 = '&#10;';
}
$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;
}
}
@@ -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;
}
}
@@ -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;
}
@@ -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);
}
}
}
@@ -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();
}
}
@@ -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);
}
}
@@ -0,0 +1,13 @@
<?php
namespace PHPHtmlParser\Dom;
/**
* Class LeafNode
*
* @package PHPHtmlParser
*/
abstract class LeafNode extends AbstractNode
{
}
@@ -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();
}
}
@@ -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.'>';
}
}
@@ -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('&#10;', "\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;
}
}
@@ -0,0 +1,12 @@
<?php
namespace PHPHtmlParser\Exceptions;
/**
* Class ChildNotFoundException
*
* @package PHPHtmlParser\Exceptions
*/
final class ChildNotFoundException extends \Exception
{
}
@@ -0,0 +1,11 @@
<?php
namespace PHPHtmlParser\Exceptions;
/**
* Class CircularException
*
* @package PHPHtmlParser\Exceptions
*/
final class CircularException extends \Exception
{
}
@@ -0,0 +1,11 @@
<?php
namespace PHPHtmlParser\Exceptions;
/**
* Class CurlException
*
* @package PHPHtmlParser\Exceptions
*/
class CurlException extends \Exception
{
}
@@ -0,0 +1,11 @@
<?php
namespace PHPHtmlParser\Exceptions;
/**
* Class EmptyCollectionException
*
* @package PHPHtmlParser\Exceptions
*/
final class EmptyCollectionException extends \Exception
{
}
@@ -0,0 +1,11 @@
<?php
namespace PHPHtmlParser\Exceptions;
/**
* Class NotLoadedException
*
* @package PHPHtmlParser\Exceptions
*/
final class NotLoadedException extends \Exception
{
}
@@ -0,0 +1,11 @@
<?php
namespace PHPHtmlParser\Exceptions;
/**
* Class ParentNotFoundException
*
* @package PHPHtmlParser\Exceptions
*/
final class ParentNotFoundException extends \Exception
{
}
@@ -0,0 +1,11 @@
<?php
namespace PHPHtmlParser\Exceptions;
/**
* Class StrictException
*
* @package PHPHtmlParser\Exceptions
*/
final class StrictException extends \Exception
{
}
@@ -0,0 +1,11 @@
<?php
namespace PHPHtmlParser\Exceptions;
/**
* Class UnknownChildTypeException
*
* @package PHPHtmlParser\Exceptions
*/
final class UnknownChildTypeException extends \Exception
{
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -0,0 +1,7 @@
<?php
namespace PHPHtmlParser\Selector;
interface ParserInterface
{
public function parseSelectorString(string $selector): array;
}
@@ -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;
}
}
@@ -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;
}
}