replace phpquery by php-dom-wrapper #3308

This replaces the outdated phpquery library by the newer and maintained
php-dom-wrapper. Usage is similar but not a 1:1 replacement. phpQuery is
left in but marked as deprecated.
This commit is contained in:
Andreas Gohr 2022-10-19 20:53:33 +02:00
parent ac2b3d9ee6
commit acdf738a61
106 changed files with 9470 additions and 65 deletions

2
.gitignore vendored
View File

@ -92,6 +92,8 @@ vendor/splitbrain/php-cli/screenshot*
vendor/splitbrain/php-cli/generate-api.sh
vendor/splitbrain/php-cli/apigen.neon
_test/vendor/*/*/tests/*
# PHPUnit tests
phpunit.phar
*.phpunit.result.cache

View File

@ -4,7 +4,8 @@
*/
if(!defined('DOKU_UNITTEST')) define('DOKU_UNITTEST',dirname(__FILE__).'/');
require_once DOKU_UNITTEST.'core/phpQuery-onefile.php';
require_once DOKU_UNITTEST.'vendor/autoload.php';
require_once DOKU_UNITTEST.'core/phpQuery-onefile.php'; // deprecated
require_once DOKU_UNITTEST.'core/DokuWikiTest.php';
require_once DOKU_UNITTEST.'core/TestResponse.php';
require_once DOKU_UNITTEST.'core/TestRequest.php';

12
_test/composer.json Normal file
View File

@ -0,0 +1,12 @@
{
"require": {
"php": ">=7.2",
"scotteh/php-dom-wrapper": "^2.0"
},
"config": {
"platform": {
"php": "7.2"
}
},
"prefer-stable": true
}

233
_test/composer.lock generated Normal file
View File

@ -0,0 +1,233 @@
{
"_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "6c1158e74baf55b1de07e592d83746a8",
"packages": [
{
"name": "scotteh/php-dom-wrapper",
"version": "2.0.3",
"source": {
"type": "git",
"url": "https://github.com/scotteh/php-dom-wrapper.git",
"reference": "edf37231a9ee609ea947ffaa5ac342a372f18b29"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/scotteh/php-dom-wrapper/zipball/edf37231a9ee609ea947ffaa5ac342a372f18b29",
"reference": "edf37231a9ee609ea947ffaa5ac342a372f18b29",
"shasum": ""
},
"require": {
"ext-libxml": "*",
"ext-mbstring": "*",
"lib-libxml": ">=2.7.7",
"php": ">=7.1.0",
"symfony/css-selector": "^4.0 || ^5.0 || ^6.0"
},
"require-dev": {
"phpunit/phpunit": "^7.0 || ^8.0 || ^9.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.0-dev"
}
},
"autoload": {
"psr-4": {
"DOMWrap\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Andrew Scott",
"email": "andrew@andrewscott.net.au"
}
],
"description": "Simple DOM wrapper to select nodes using either CSS or XPath expressions and manipulate results quickly and easily.",
"homepage": "https://github.com/scotteh/php-dom-wrapper",
"keywords": [
"css",
"dom",
"html",
"parser",
"wrapper"
],
"support": {
"issues": "https://github.com/scotteh/php-dom-wrapper/issues",
"source": "https://github.com/scotteh/php-dom-wrapper/tree/2.0.3"
},
"time": "2022-05-14T06:10:52+00:00"
},
{
"name": "symfony/css-selector",
"version": "v4.4.44",
"source": {
"type": "git",
"url": "https://github.com/symfony/css-selector.git",
"reference": "bd0a6737e48de45b4b0b7b6fc98c78404ddceaed"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/css-selector/zipball/bd0a6737e48de45b4b0b7b6fc98c78404ddceaed",
"reference": "bd0a6737e48de45b4b0b7b6fc98c78404ddceaed",
"shasum": ""
},
"require": {
"php": ">=7.1.3",
"symfony/polyfill-php80": "^1.16"
},
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\CssSelector\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Jean-François Simon",
"email": "jeanfrancois.simon@sensiolabs.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Converts CSS selectors to XPath expressions",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/css-selector/tree/v4.4.44"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2022-06-27T13:16:42+00:00"
},
{
"name": "symfony/polyfill-php80",
"version": "v1.26.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php80.git",
"reference": "cfa0ae98841b9e461207c13ab093d76b0fa7bace"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/cfa0ae98841b9e461207c13ab093d76b0fa7bace",
"reference": "cfa0ae98841b9e461207c13ab093d76b0fa7bace",
"shasum": ""
},
"require": {
"php": ">=7.1"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.26-dev"
},
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
}
},
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Php80\\": ""
},
"classmap": [
"Resources/stubs"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Ion Bazan",
"email": "ion.bazan@gmail.com"
},
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"polyfill",
"portable",
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-php80/tree/v1.26.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2022-05-10T07:21:04+00:00"
}
],
"packages-dev": [],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": [],
"prefer-stable": true,
"prefer-lowest": false,
"platform": {
"php": ">=7.2"
},
"platform-dev": [],
"platform-overrides": {
"php": "7.2"
},
"plugin-api-version": "2.3.0"
}

View File

@ -97,7 +97,10 @@ class TestResponse {
* @return phpQueryObject
*/
public function queryHTML($selector) {
if(is_null($this->pq)) $this->pq = phpQuery::newDocument($this->content);
if(is_null($this->pq)) {
$this->pq = new \DOMWrap\Document();
$this->pq->html($this->content);
}
return $this->pq->find($selector);
}

View File

@ -10,6 +10,7 @@
* @author Tobiasz Cudnik <tobiasz.cudnik/gmail.com>
* @license http://www.opensource.org/licenses/mit-license.php MIT License
* @package phpQuery
* @deprecated 2022-10-19
*/
// class names for instanceof
@ -1184,6 +1185,8 @@ class phpQueryObject
* @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
*/
public function __construct($documentID) {
dbg_deprecated(\DOMWrap\Document::class);
// if ($documentID instanceof self)
// var_dump($documentID->getDocumentID());
$id = $documentID instanceof self

View File

@ -1,6 +1,7 @@
<?php
use dokuwiki\Form;
use DOMWrap\Document;
class form_buttonelement_test extends DokuWikiTest {
@ -9,16 +10,16 @@ class form_buttonelement_test extends DokuWikiTest {
$form->addButton('foo', 'Hello <b>World</b>')->val('bam')->attr('type', 'submit');
$html = $form->toHTML();
$pq = phpQuery::newDocumentXHTML($html);
$pq = (new Document())->html($html);
$input = $pq->find('button[name=foo]');
$this->assertTrue($input->length == 1);
$this->assertEquals('bam', $input->val());
$this->assertTrue($input->count() == 1);
$this->assertEquals('bam', $input->attr('value'));
$this->assertEquals('submit', $input->attr('type'));
$this->assertEquals('Hello <b>World</b>', $input->text()); // tags were escaped
$b = $input->find('b'); // no tags found
$this->assertTrue($b->length == 0);
$this->assertTrue($b->count() == 0);
}
function test_html() {
@ -26,15 +27,15 @@ class form_buttonelement_test extends DokuWikiTest {
$form->addButtonHTML('foo', 'Hello <b>World</b>')->val('bam')->attr('type', 'submit');
$html = $form->toHTML();
$pq = phpQuery::newDocumentXHTML($html);
$pq = (new Document())->html($html);
$input = $pq->find('button[name=foo]');
$this->assertTrue($input->length == 1);
$this->assertEquals('bam', $input->val());
$this->assertTrue($input->count() == 1);
$this->assertEquals('bam', $input->attr('value'));
$this->assertEquals('submit', $input->attr('type'));
$this->assertEquals('Hello World', $input->text()); // tags are stripped here
$b = $input->find('b'); // tags found
$this->assertTrue($b->length == 1);
$this->assertTrue($b->count() == 1);
}
}

View File

@ -1,6 +1,7 @@
<?php
use dokuwiki\Form;
use DOMWrap\Document;
class form_checkableelement_test extends DokuWikiTest {
@ -10,20 +11,20 @@ class form_checkableelement_test extends DokuWikiTest {
$form->addRadioButton('foo', 'label text second')->val('second');
$html = $form->toHTML();
$pq = phpQuery::newDocumentXHTML($html);
$pq = (new Document())->html($html);
$input = $pq->find('input[name=foo]');
$this->assertTrue($input->length == 2);
$this->assertTrue($input->count() == 2);
$label = $pq->find('label');
$this->assertTrue($label->length == 2);
$this->assertTrue($label->count() == 2);
$inputs = $pq->find('input[name=foo]');
$this->assertEquals('first', pq($inputs->elements[0])->val());
$this->assertEquals('second', pq($inputs->elements[1])->val());
$this->assertEquals('checked', pq($inputs->elements[0])->attr('checked'));
$this->assertEquals('', pq($inputs->elements[1])->attr('checked'));
$this->assertEquals('radio', pq($inputs->elements[0])->attr('type'));
$this->assertEquals('first', $inputs->get(0)->attr('value'));
$this->assertEquals('second', $inputs->get(1)->attr('value'));
$this->assertEquals('checked', $inputs->get(0)->attr('checked'));
$this->assertEquals('', $inputs->get(1)->attr('checked'));
$this->assertEquals('radio', $inputs->get(0)->attr('type'));
}
/**
@ -39,13 +40,13 @@ class form_checkableelement_test extends DokuWikiTest {
$form->addRadioButton('foo', 'label text second')->val('second');
$html = $form->toHTML();
$pq = phpQuery::newDocumentXHTML($html);
$pq = (new Document())->html($html);
$inputs = $pq->find('input[name=foo]');
$this->assertEquals('first', pq($inputs->elements[0])->val());
$this->assertEquals('second', pq($inputs->elements[1])->val());
$this->assertEquals('', pq($inputs->elements[0])->attr('checked'));
$this->assertEquals('checked', pq($inputs->elements[1])->attr('checked'));
$this->assertEquals('radio', pq($inputs->elements[0])->attr('type'));
$this->assertEquals('first', $inputs->get(0)->attr('value'));
$this->assertEquals('second', $inputs->get(1)->attr('value'));
$this->assertEquals('', $inputs->get(0)->attr('checked'));
$this->assertEquals('checked', $inputs->get(1)->attr('checked'));
$this->assertEquals('radio', $inputs->get(0)->attr('type'));
}
}

View File

@ -1,6 +1,7 @@
<?php
use dokuwiki\Form;
use DOMWrap\Document;
class form_dropdownelement_test extends DokuWikiTest {
@ -27,22 +28,21 @@ class form_dropdownelement_test extends DokuWikiTest {
// HTML
$html = $form->toHTML();
$pq = phpQuery::newDocumentXHTML($html);
$pq = (new Document())->html($html);
$select = $pq->find('select[name=foo]');
$this->assertTrue($select->length == 1);
$this->assertEquals('first', $select->val());
$this->assertTrue($select->count() == 1);
$options = $pq->find('option');
$this->assertTrue($options->length == 3);
$this->assertTrue($options->count() == 3);
$option = $pq->find('option[selected=selected]');
$this->assertTrue($option->length == 1);
$this->assertEquals('first', $option->val());
$this->assertTrue($option->count() == 1);
$this->assertEquals('first', $option->attr('value'));
$this->assertEquals('A first Label', $option->text());
$label = $pq->find('label');
$this->assertTrue($label->length == 1);
$this->assertTrue($label->count() == 1);
$this->assertEquals('label text', $label->find('span')->text());
}
@ -67,17 +67,17 @@ class form_dropdownelement_test extends DokuWikiTest {
$form->addDropdown('foo', $options, 'label text');
// HTML
$html = $form->toHTML();
$pq = phpQuery::newDocumentXHTML($html);
$pq = (new Document())->html($html);;
$select = $pq->find('select[name=foo]');
$this->assertTrue($select->length == 1);
$this->assertTrue($select->count() == 1);
$options = $pq->find('option');
$this->assertEquals(3, $options->length);
$this->assertEquals(3, $options->count());
$option = $pq->find('option#theID');
$this->assertEquals(1, $option->length);
$this->assertEquals('first', $option->val());
$this->assertEquals(1, $option->count());
$this->assertEquals('first', $option->attr('value'));
$this->assertEquals('the label', $option->text());
$this->assertEquals('bar', $option->attr('data-foo'));
$this->assertTrue($option->hasClass('two'));
@ -116,16 +116,16 @@ class form_dropdownelement_test extends DokuWikiTest {
// HTML
$html = $form->toHTML();
$pq = phpQuery::newDocumentXHTML($html);
$pq = (new Document())->html($html);
$optGroupsHTML = $pq->find('optgroup');
$this->assertEquals(2, $optGroupsHTML->length);
$this->assertEquals(2, $optGroupsHTML->count());
$options = $pq->find('option');
$this->assertEquals(4, $options->length);
$this->assertEquals(4, $options->count());
$selected = $pq->find('option[selected=selected]');
$this->assertEquals('third', $selected->val());
$this->assertEquals('third', $selected->attr('value'));
$this->assertEquals('label of third option', $selected->text());
}
@ -138,9 +138,9 @@ class form_dropdownelement_test extends DokuWikiTest {
$html = $form->toHTML();
$pq = phpQuery::newDocumentXHTML($html);
$pq = (new Document())->html($html);
$selected = $pq->find('option[selected=selected]');
$this->assertEquals(1, $selected->length);
$this->assertEquals(1, $selected->count());
$this->assertEquals('Auto', $selected->text());
}
@ -167,9 +167,9 @@ class form_dropdownelement_test extends DokuWikiTest {
// HTML
$html = $form->toHTML();
$pq = phpQuery::newDocumentXHTML($html);
$pq = (new Document())->html($html);
$selected = $pq->find('option[selected=selected]');
$this->assertEquals(1, $selected->length);
$this->assertEquals(1, $selected->count());
$this->assertEquals('the label', $selected->text());
}
@ -187,11 +187,11 @@ class form_dropdownelement_test extends DokuWikiTest {
$this->assertEquals('third', $element->val());
$html = $form->toHTML();
$pq = phpQuery::newDocumentXHTML($html);
$pq = (new Document())->html($html);
$option = $pq->find('option[selected=selected]');
$this->assertTrue($option->length == 1);
$this->assertEquals('second', $option->val());
$this->assertTrue($option->count() == 1);
$this->assertEquals('second', $option->attr('value'));
$this->assertEquals('The second Label', $option->text());
}
@ -208,7 +208,7 @@ class form_dropdownelement_test extends DokuWikiTest {
// check HTML
$html = $form->toHTML();
$pq = phpQuery::newDocumentXHTML($html);
$pq = (new Document())->html($html);;
$option = $pq->find('option[selected=selected]');
$this->assertEquals('A first Label', $option->get(0)->textContent);

View File

@ -1,6 +1,7 @@
<?php
use dokuwiki\Form;
use DOMWrap\Document;
/**
* makes form internals accessible for testing
@ -35,13 +36,13 @@ class form_form_test extends DokuWikiTest {
$form = new Form\Form();
$html = $form->toHTML();
$pq = phpQuery::newDocumentXHTML($html);
$pq = (new Document())->html($html);
$this->assertTrue($pq->find('form')->hasClass('doku_form'));
$this->assertEquals(wl($ID, array('foo' => 'bar'), false, '&'), $pq->find('form')->attr('action'));
$this->assertEquals('post', $pq->find('form')->attr('method'));
$this->assertTrue($pq->find('input[name=sectok]')->length == 1);
$this->assertTrue($pq->find('input[name=sectok]')->count() == 1);
}

View File

@ -1,6 +1,7 @@
<?php
use dokuwiki\Form;
use DOMWrap\Document;
class form_inputelement_test extends DokuWikiTest {
@ -9,15 +10,15 @@ class form_inputelement_test extends DokuWikiTest {
$form->addTextInput('foo', 'label text')->val('this is text');
$html = $form->toHTML();
$pq = phpQuery::newDocumentXHTML($html);
$pq = (new Document())->html($html);
$input = $pq->find('input[name=foo]');
$this->assertTrue($input->length == 1);
$this->assertEquals('this is text', $input->val());
$this->assertTrue($input->count() == 1);
$this->assertEquals('this is text', $input->attr('value'));
$this->assertEquals('text', $input->attr('type'));
$label = $pq->find('label');
$this->assertTrue($label->length == 1);
$this->assertTrue($label->count() == 1);
$this->assertEquals('label text', $label->find('span')->text());
}
@ -32,11 +33,11 @@ class form_inputelement_test extends DokuWikiTest {
$form->addTextInput('foo', 'label text')->val('this is text');
$html = $form->toHTML();
$pq = phpQuery::newDocumentXHTML($html);
$pq = (new Document())->html($html);
$input = $pq->find('input[name=foo]');
$this->assertTrue($input->length == 1);
$this->assertEquals('a new text', $input->val());
$this->assertTrue($input->count() == 1);
$this->assertEquals('a new text', $input->attr('value'));
}
function test_prefill_empty() {
@ -47,11 +48,11 @@ class form_inputelement_test extends DokuWikiTest {
$form->addTextInput('foo', 'label text')->val('this is text');
$html = $form->toHTML();
$pq = phpQuery::newDocumentXHTML($html);
$pq = (new Document())->html($html);
$input = $pq->find('input[name=foo]');
$this->assertTrue($input->length == 1);
$this->assertEquals('', $input->val());
$this->assertTrue($input->count() == 1);
$this->assertEquals('', $input->attr('value'));
}
@ -60,15 +61,15 @@ class form_inputelement_test extends DokuWikiTest {
$form->addPasswordInput('foo', 'label text')->val('this is text');
$html = $form->toHTML();
$pq = phpQuery::newDocumentXHTML($html);
$pq = (new Document())->html($html);
$input = $pq->find('input[name=foo]');
$this->assertTrue($input->length == 1);
$this->assertEquals('this is text', $input->val());
$this->assertTrue($input->count() == 1);
$this->assertEquals('this is text', $input->attr('value'));
$this->assertEquals('password', $input->attr('type'));
$label = $pq->find('label');
$this->assertTrue($label->length == 1);
$this->assertTrue($label->count() == 1);
$this->assertEquals('label text', $label->find('span')->text());
}
}

View File

@ -1,5 +1,7 @@
<?php
use DOMWrap\Document;
class media_searchlist_test extends DokuWikiTest
{
@ -94,7 +96,7 @@ class media_searchlist_test extends DokuWikiTest
public function testSearch($query, $expected)
{
$result = $this->media_searchlist($query, $this->upload_ns);
$pq = phpQuery::newDocument($result);
$pq = (new Document())->html($result);
$elements = $pq->find('a.mediafile');
$actual = [];

12
_test/vendor/autoload.php vendored Normal file
View File

@ -0,0 +1,12 @@
<?php
// autoload.php @generated by Composer
if (PHP_VERSION_ID < 50600) {
echo 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL;
exit(1);
}
require_once __DIR__ . '/composer/autoload_real.php';
return ComposerAutoloaderInit19ac79df47ecd2a96d0c835fd95d6405::getLoader();

572
_test/vendor/composer/ClassLoader.php vendored Normal file
View File

@ -0,0 +1,572 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer\Autoload;
/**
* ClassLoader implements a PSR-0, PSR-4 and classmap class loader.
*
* $loader = new \Composer\Autoload\ClassLoader();
*
* // register classes with namespaces
* $loader->add('Symfony\Component', __DIR__.'/component');
* $loader->add('Symfony', __DIR__.'/framework');
*
* // activate the autoloader
* $loader->register();
*
* // to enable searching the include path (eg. for PEAR packages)
* $loader->setUseIncludePath(true);
*
* In this example, if you try to use a class in the Symfony\Component
* namespace or one of its children (Symfony\Component\Console for instance),
* the autoloader will first look for the class under the component/
* directory, and it will then fallback to the framework/ directory if not
* found before giving up.
*
* This class is loosely based on the Symfony UniversalClassLoader.
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Jordi Boggiano <j.boggiano@seld.be>
* @see https://www.php-fig.org/psr/psr-0/
* @see https://www.php-fig.org/psr/psr-4/
*/
class ClassLoader
{
/** @var ?string */
private $vendorDir;
// PSR-4
/**
* @var array[]
* @psalm-var array<string, array<string, int>>
*/
private $prefixLengthsPsr4 = array();
/**
* @var array[]
* @psalm-var array<string, array<int, string>>
*/
private $prefixDirsPsr4 = array();
/**
* @var array[]
* @psalm-var array<string, string>
*/
private $fallbackDirsPsr4 = array();
// PSR-0
/**
* @var array[]
* @psalm-var array<string, array<string, string[]>>
*/
private $prefixesPsr0 = array();
/**
* @var array[]
* @psalm-var array<string, string>
*/
private $fallbackDirsPsr0 = array();
/** @var bool */
private $useIncludePath = false;
/**
* @var string[]
* @psalm-var array<string, string>
*/
private $classMap = array();
/** @var bool */
private $classMapAuthoritative = false;
/**
* @var bool[]
* @psalm-var array<string, bool>
*/
private $missingClasses = array();
/** @var ?string */
private $apcuPrefix;
/**
* @var self[]
*/
private static $registeredLoaders = array();
/**
* @param ?string $vendorDir
*/
public function __construct($vendorDir = null)
{
$this->vendorDir = $vendorDir;
}
/**
* @return string[]
*/
public function getPrefixes()
{
if (!empty($this->prefixesPsr0)) {
return call_user_func_array('array_merge', array_values($this->prefixesPsr0));
}
return array();
}
/**
* @return array[]
* @psalm-return array<string, array<int, string>>
*/
public function getPrefixesPsr4()
{
return $this->prefixDirsPsr4;
}
/**
* @return array[]
* @psalm-return array<string, string>
*/
public function getFallbackDirs()
{
return $this->fallbackDirsPsr0;
}
/**
* @return array[]
* @psalm-return array<string, string>
*/
public function getFallbackDirsPsr4()
{
return $this->fallbackDirsPsr4;
}
/**
* @return string[] Array of classname => path
* @psalm-return array<string, string>
*/
public function getClassMap()
{
return $this->classMap;
}
/**
* @param string[] $classMap Class to filename map
* @psalm-param array<string, string> $classMap
*
* @return void
*/
public function addClassMap(array $classMap)
{
if ($this->classMap) {
$this->classMap = array_merge($this->classMap, $classMap);
} else {
$this->classMap = $classMap;
}
}
/**
* Registers a set of PSR-0 directories for a given prefix, either
* appending or prepending to the ones previously set for this prefix.
*
* @param string $prefix The prefix
* @param string[]|string $paths The PSR-0 root directories
* @param bool $prepend Whether to prepend the directories
*
* @return void
*/
public function add($prefix, $paths, $prepend = false)
{
if (!$prefix) {
if ($prepend) {
$this->fallbackDirsPsr0 = array_merge(
(array) $paths,
$this->fallbackDirsPsr0
);
} else {
$this->fallbackDirsPsr0 = array_merge(
$this->fallbackDirsPsr0,
(array) $paths
);
}
return;
}
$first = $prefix[0];
if (!isset($this->prefixesPsr0[$first][$prefix])) {
$this->prefixesPsr0[$first][$prefix] = (array) $paths;
return;
}
if ($prepend) {
$this->prefixesPsr0[$first][$prefix] = array_merge(
(array) $paths,
$this->prefixesPsr0[$first][$prefix]
);
} else {
$this->prefixesPsr0[$first][$prefix] = array_merge(
$this->prefixesPsr0[$first][$prefix],
(array) $paths
);
}
}
/**
* Registers a set of PSR-4 directories for a given namespace, either
* appending or prepending to the ones previously set for this namespace.
*
* @param string $prefix The prefix/namespace, with trailing '\\'
* @param string[]|string $paths The PSR-4 base directories
* @param bool $prepend Whether to prepend the directories
*
* @throws \InvalidArgumentException
*
* @return void
*/
public function addPsr4($prefix, $paths, $prepend = false)
{
if (!$prefix) {
// Register directories for the root namespace.
if ($prepend) {
$this->fallbackDirsPsr4 = array_merge(
(array) $paths,
$this->fallbackDirsPsr4
);
} else {
$this->fallbackDirsPsr4 = array_merge(
$this->fallbackDirsPsr4,
(array) $paths
);
}
} elseif (!isset($this->prefixDirsPsr4[$prefix])) {
// Register directories for a new namespace.
$length = strlen($prefix);
if ('\\' !== $prefix[$length - 1]) {
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
}
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
$this->prefixDirsPsr4[$prefix] = (array) $paths;
} elseif ($prepend) {
// Prepend directories for an already registered namespace.
$this->prefixDirsPsr4[$prefix] = array_merge(
(array) $paths,
$this->prefixDirsPsr4[$prefix]
);
} else {
// Append directories for an already registered namespace.
$this->prefixDirsPsr4[$prefix] = array_merge(
$this->prefixDirsPsr4[$prefix],
(array) $paths
);
}
}
/**
* Registers a set of PSR-0 directories for a given prefix,
* replacing any others previously set for this prefix.
*
* @param string $prefix The prefix
* @param string[]|string $paths The PSR-0 base directories
*
* @return void
*/
public function set($prefix, $paths)
{
if (!$prefix) {
$this->fallbackDirsPsr0 = (array) $paths;
} else {
$this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths;
}
}
/**
* Registers a set of PSR-4 directories for a given namespace,
* replacing any others previously set for this namespace.
*
* @param string $prefix The prefix/namespace, with trailing '\\'
* @param string[]|string $paths The PSR-4 base directories
*
* @throws \InvalidArgumentException
*
* @return void
*/
public function setPsr4($prefix, $paths)
{
if (!$prefix) {
$this->fallbackDirsPsr4 = (array) $paths;
} else {
$length = strlen($prefix);
if ('\\' !== $prefix[$length - 1]) {
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
}
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
$this->prefixDirsPsr4[$prefix] = (array) $paths;
}
}
/**
* Turns on searching the include path for class files.
*
* @param bool $useIncludePath
*
* @return void
*/
public function setUseIncludePath($useIncludePath)
{
$this->useIncludePath = $useIncludePath;
}
/**
* Can be used to check if the autoloader uses the include path to check
* for classes.
*
* @return bool
*/
public function getUseIncludePath()
{
return $this->useIncludePath;
}
/**
* Turns off searching the prefix and fallback directories for classes
* that have not been registered with the class map.
*
* @param bool $classMapAuthoritative
*
* @return void
*/
public function setClassMapAuthoritative($classMapAuthoritative)
{
$this->classMapAuthoritative = $classMapAuthoritative;
}
/**
* Should class lookup fail if not found in the current class map?
*
* @return bool
*/
public function isClassMapAuthoritative()
{
return $this->classMapAuthoritative;
}
/**
* APCu prefix to use to cache found/not-found classes, if the extension is enabled.
*
* @param string|null $apcuPrefix
*
* @return void
*/
public function setApcuPrefix($apcuPrefix)
{
$this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null;
}
/**
* The APCu prefix in use, or null if APCu caching is not enabled.
*
* @return string|null
*/
public function getApcuPrefix()
{
return $this->apcuPrefix;
}
/**
* Registers this instance as an autoloader.
*
* @param bool $prepend Whether to prepend the autoloader or not
*
* @return void
*/
public function register($prepend = false)
{
spl_autoload_register(array($this, 'loadClass'), true, $prepend);
if (null === $this->vendorDir) {
return;
}
if ($prepend) {
self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders;
} else {
unset(self::$registeredLoaders[$this->vendorDir]);
self::$registeredLoaders[$this->vendorDir] = $this;
}
}
/**
* Unregisters this instance as an autoloader.
*
* @return void
*/
public function unregister()
{
spl_autoload_unregister(array($this, 'loadClass'));
if (null !== $this->vendorDir) {
unset(self::$registeredLoaders[$this->vendorDir]);
}
}
/**
* Loads the given class or interface.
*
* @param string $class The name of the class
* @return true|null True if loaded, null otherwise
*/
public function loadClass($class)
{
if ($file = $this->findFile($class)) {
includeFile($file);
return true;
}
return null;
}
/**
* Finds the path to the file where the class is defined.
*
* @param string $class The name of the class
*
* @return string|false The path if found, false otherwise
*/
public function findFile($class)
{
// class map lookup
if (isset($this->classMap[$class])) {
return $this->classMap[$class];
}
if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) {
return false;
}
if (null !== $this->apcuPrefix) {
$file = apcu_fetch($this->apcuPrefix.$class, $hit);
if ($hit) {
return $file;
}
}
$file = $this->findFileWithExtension($class, '.php');
// Search for Hack files if we are running on HHVM
if (false === $file && defined('HHVM_VERSION')) {
$file = $this->findFileWithExtension($class, '.hh');
}
if (null !== $this->apcuPrefix) {
apcu_add($this->apcuPrefix.$class, $file);
}
if (false === $file) {
// Remember that this class does not exist.
$this->missingClasses[$class] = true;
}
return $file;
}
/**
* Returns the currently registered loaders indexed by their corresponding vendor directories.
*
* @return self[]
*/
public static function getRegisteredLoaders()
{
return self::$registeredLoaders;
}
/**
* @param string $class
* @param string $ext
* @return string|false
*/
private function findFileWithExtension($class, $ext)
{
// PSR-4 lookup
$logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
$first = $class[0];
if (isset($this->prefixLengthsPsr4[$first])) {
$subPath = $class;
while (false !== $lastPos = strrpos($subPath, '\\')) {
$subPath = substr($subPath, 0, $lastPos);
$search = $subPath . '\\';
if (isset($this->prefixDirsPsr4[$search])) {
$pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1);
foreach ($this->prefixDirsPsr4[$search] as $dir) {
if (file_exists($file = $dir . $pathEnd)) {
return $file;
}
}
}
}
}
// PSR-4 fallback dirs
foreach ($this->fallbackDirsPsr4 as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
return $file;
}
}
// PSR-0 lookup
if (false !== $pos = strrpos($class, '\\')) {
// namespaced class name
$logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)
. strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
} else {
// PEAR-like class name
$logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext;
}
if (isset($this->prefixesPsr0[$first])) {
foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
if (0 === strpos($class, $prefix)) {
foreach ($dirs as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
return $file;
}
}
}
}
}
// PSR-0 fallback dirs
foreach ($this->fallbackDirsPsr0 as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
return $file;
}
}
// PSR-0 include paths.
if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
return $file;
}
return false;
}
}
/**
* Scope isolated include.
*
* Prevents access to $this/self from included files.
*
* @param string $file
* @return void
* @private
*/
function includeFile($file)
{
include $file;
}

View File

@ -0,0 +1,352 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer;
use Composer\Autoload\ClassLoader;
use Composer\Semver\VersionParser;
/**
* This class is copied in every Composer installed project and available to all
*
* See also https://getcomposer.org/doc/07-runtime.md#installed-versions
*
* To require its presence, you can require `composer-runtime-api ^2.0`
*
* @final
*/
class InstalledVersions
{
/**
* @var mixed[]|null
* @psalm-var array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}, versions: array<string, array{dev_requirement: bool, pretty_version?: string, version?: string, aliases?: string[], reference?: string, replaced?: string[], provided?: string[], install_path?: string, type?: string}>}|array{}|null
*/
private static $installed;
/**
* @var bool|null
*/
private static $canGetVendors;
/**
* @var array[]
* @psalm-var array<string, array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}, versions: array<string, array{dev_requirement: bool, pretty_version?: string, version?: string, aliases?: string[], reference?: string, replaced?: string[], provided?: string[], install_path?: string, type?: string}>}>
*/
private static $installedByVendor = array();
/**
* Returns a list of all package names which are present, either by being installed, replaced or provided
*
* @return string[]
* @psalm-return list<string>
*/
public static function getInstalledPackages()
{
$packages = array();
foreach (self::getInstalled() as $installed) {
$packages[] = array_keys($installed['versions']);
}
if (1 === \count($packages)) {
return $packages[0];
}
return array_keys(array_flip(\call_user_func_array('array_merge', $packages)));
}
/**
* Returns a list of all package names with a specific type e.g. 'library'
*
* @param string $type
* @return string[]
* @psalm-return list<string>
*/
public static function getInstalledPackagesByType($type)
{
$packagesByType = array();
foreach (self::getInstalled() as $installed) {
foreach ($installed['versions'] as $name => $package) {
if (isset($package['type']) && $package['type'] === $type) {
$packagesByType[] = $name;
}
}
}
return $packagesByType;
}
/**
* Checks whether the given package is installed
*
* This also returns true if the package name is provided or replaced by another package
*
* @param string $packageName
* @param bool $includeDevRequirements
* @return bool
*/
public static function isInstalled($packageName, $includeDevRequirements = true)
{
foreach (self::getInstalled() as $installed) {
if (isset($installed['versions'][$packageName])) {
return $includeDevRequirements || empty($installed['versions'][$packageName]['dev_requirement']);
}
}
return false;
}
/**
* Checks whether the given package satisfies a version constraint
*
* e.g. If you want to know whether version 2.3+ of package foo/bar is installed, you would call:
*
* Composer\InstalledVersions::satisfies(new VersionParser, 'foo/bar', '^2.3')
*
* @param VersionParser $parser Install composer/semver to have access to this class and functionality
* @param string $packageName
* @param string|null $constraint A version constraint to check for, if you pass one you have to make sure composer/semver is required by your package
* @return bool
*/
public static function satisfies(VersionParser $parser, $packageName, $constraint)
{
$constraint = $parser->parseConstraints($constraint);
$provided = $parser->parseConstraints(self::getVersionRanges($packageName));
return $provided->matches($constraint);
}
/**
* Returns a version constraint representing all the range(s) which are installed for a given package
*
* It is easier to use this via isInstalled() with the $constraint argument if you need to check
* whether a given version of a package is installed, and not just whether it exists
*
* @param string $packageName
* @return string Version constraint usable with composer/semver
*/
public static function getVersionRanges($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
$ranges = array();
if (isset($installed['versions'][$packageName]['pretty_version'])) {
$ranges[] = $installed['versions'][$packageName]['pretty_version'];
}
if (array_key_exists('aliases', $installed['versions'][$packageName])) {
$ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']);
}
if (array_key_exists('replaced', $installed['versions'][$packageName])) {
$ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']);
}
if (array_key_exists('provided', $installed['versions'][$packageName])) {
$ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']);
}
return implode(' || ', $ranges);
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
*/
public static function getVersion($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
if (!isset($installed['versions'][$packageName]['version'])) {
return null;
}
return $installed['versions'][$packageName]['version'];
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
*/
public static function getPrettyVersion($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
if (!isset($installed['versions'][$packageName]['pretty_version'])) {
return null;
}
return $installed['versions'][$packageName]['pretty_version'];
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as reference
*/
public static function getReference($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
if (!isset($installed['versions'][$packageName]['reference'])) {
return null;
}
return $installed['versions'][$packageName]['reference'];
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as install path. Packages of type metapackages also have a null install path.
*/
public static function getInstallPath($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
return isset($installed['versions'][$packageName]['install_path']) ? $installed['versions'][$packageName]['install_path'] : null;
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @return array
* @psalm-return array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}
*/
public static function getRootPackage()
{
$installed = self::getInstalled();
return $installed[0]['root'];
}
/**
* Returns the raw installed.php data for custom implementations
*
* @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect.
* @return array[]
* @psalm-return array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}, versions: array<string, array{dev_requirement: bool, pretty_version?: string, version?: string, aliases?: string[], reference?: string, replaced?: string[], provided?: string[], install_path?: string, type?: string}>}
*/
public static function getRawData()
{
@trigger_error('getRawData only returns the first dataset loaded, which may not be what you expect. Use getAllRawData() instead which returns all datasets for all autoloaders present in the process.', E_USER_DEPRECATED);
if (null === self::$installed) {
// only require the installed.php file if this file is loaded from its dumped location,
// and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
if (substr(__DIR__, -8, 1) !== 'C') {
self::$installed = include __DIR__ . '/installed.php';
} else {
self::$installed = array();
}
}
return self::$installed;
}
/**
* Returns the raw data of all installed.php which are currently loaded for custom implementations
*
* @return array[]
* @psalm-return list<array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}, versions: array<string, array{dev_requirement: bool, pretty_version?: string, version?: string, aliases?: string[], reference?: string, replaced?: string[], provided?: string[], install_path?: string, type?: string}>}>
*/
public static function getAllRawData()
{
return self::getInstalled();
}
/**
* Lets you reload the static array from another file
*
* This is only useful for complex integrations in which a project needs to use
* this class but then also needs to execute another project's autoloader in process,
* and wants to ensure both projects have access to their version of installed.php.
*
* A typical case would be PHPUnit, where it would need to make sure it reads all
* the data it needs from this class, then call reload() with
* `require $CWD/vendor/composer/installed.php` (or similar) as input to make sure
* the project in which it runs can then also use this class safely, without
* interference between PHPUnit's dependencies and the project's dependencies.
*
* @param array[] $data A vendor/composer/installed.php data set
* @return void
*
* @psalm-param array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}, versions: array<string, array{dev_requirement: bool, pretty_version?: string, version?: string, aliases?: string[], reference?: string, replaced?: string[], provided?: string[], install_path?: string, type?: string}>} $data
*/
public static function reload($data)
{
self::$installed = $data;
self::$installedByVendor = array();
}
/**
* @return array[]
* @psalm-return list<array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}, versions: array<string, array{dev_requirement: bool, pretty_version?: string, version?: string, aliases?: string[], reference?: string, replaced?: string[], provided?: string[], install_path?: string, type?: string}>}>
*/
private static function getInstalled()
{
if (null === self::$canGetVendors) {
self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders');
}
$installed = array();
if (self::$canGetVendors) {
foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
if (isset(self::$installedByVendor[$vendorDir])) {
$installed[] = self::$installedByVendor[$vendorDir];
} elseif (is_file($vendorDir.'/composer/installed.php')) {
$installed[] = self::$installedByVendor[$vendorDir] = require $vendorDir.'/composer/installed.php';
if (null === self::$installed && strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) {
self::$installed = $installed[count($installed) - 1];
}
}
}
}
if (null === self::$installed) {
// only require the installed.php file if this file is loaded from its dumped location,
// and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
if (substr(__DIR__, -8, 1) !== 'C') {
self::$installed = require __DIR__ . '/installed.php';
} else {
self::$installed = array();
}
}
$installed[] = self::$installed;
return $installed;
}
}

21
_test/vendor/composer/LICENSE vendored Normal file
View File

@ -0,0 +1,21 @@
Copyright (c) Nils Adermann, Jordi Boggiano
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@ -0,0 +1,15 @@
<?php
// autoload_classmap.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
'Attribute' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/Attribute.php',
'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',
'PhpToken' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/PhpToken.php',
'Stringable' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/Stringable.php',
'UnhandledMatchError' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/UnhandledMatchError.php',
'ValueError' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/ValueError.php',
);

View File

@ -0,0 +1,10 @@
<?php
// autoload_files.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
'a4a119a56e50fbb293281d9a48007e0e' => $vendorDir . '/symfony/polyfill-php80/bootstrap.php',
);

View File

@ -0,0 +1,9 @@
<?php
// autoload_namespaces.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
);

12
_test/vendor/composer/autoload_psr4.php vendored Normal file
View File

@ -0,0 +1,12 @@
<?php
// autoload_psr4.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
'Symfony\\Polyfill\\Php80\\' => array($vendorDir . '/symfony/polyfill-php80'),
'Symfony\\Component\\CssSelector\\' => array($vendorDir . '/symfony/css-selector'),
'DOMWrap\\' => array($vendorDir . '/scotteh/php-dom-wrapper/src'),
);

57
_test/vendor/composer/autoload_real.php vendored Normal file
View File

@ -0,0 +1,57 @@
<?php
// autoload_real.php @generated by Composer
class ComposerAutoloaderInit19ac79df47ecd2a96d0c835fd95d6405
{
private static $loader;
public static function loadClassLoader($class)
{
if ('Composer\Autoload\ClassLoader' === $class) {
require __DIR__ . '/ClassLoader.php';
}
}
/**
* @return \Composer\Autoload\ClassLoader
*/
public static function getLoader()
{
if (null !== self::$loader) {
return self::$loader;
}
require __DIR__ . '/platform_check.php';
spl_autoload_register(array('ComposerAutoloaderInit19ac79df47ecd2a96d0c835fd95d6405', 'loadClassLoader'), true, true);
self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(__DIR__));
spl_autoload_unregister(array('ComposerAutoloaderInit19ac79df47ecd2a96d0c835fd95d6405', 'loadClassLoader'));
require __DIR__ . '/autoload_static.php';
call_user_func(\Composer\Autoload\ComposerStaticInit19ac79df47ecd2a96d0c835fd95d6405::getInitializer($loader));
$loader->register(true);
$includeFiles = \Composer\Autoload\ComposerStaticInit19ac79df47ecd2a96d0c835fd95d6405::$files;
foreach ($includeFiles as $fileIdentifier => $file) {
composerRequire19ac79df47ecd2a96d0c835fd95d6405($fileIdentifier, $file);
}
return $loader;
}
}
/**
* @param string $fileIdentifier
* @param string $file
* @return void
*/
function composerRequire19ac79df47ecd2a96d0c835fd95d6405($fileIdentifier, $file)
{
if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
$GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
require $file;
}
}

View File

@ -0,0 +1,58 @@
<?php
// autoload_static.php @generated by Composer
namespace Composer\Autoload;
class ComposerStaticInit19ac79df47ecd2a96d0c835fd95d6405
{
public static $files = array (
'a4a119a56e50fbb293281d9a48007e0e' => __DIR__ . '/..' . '/symfony/polyfill-php80/bootstrap.php',
);
public static $prefixLengthsPsr4 = array (
'S' =>
array (
'Symfony\\Polyfill\\Php80\\' => 23,
'Symfony\\Component\\CssSelector\\' => 30,
),
'D' =>
array (
'DOMWrap\\' => 8,
),
);
public static $prefixDirsPsr4 = array (
'Symfony\\Polyfill\\Php80\\' =>
array (
0 => __DIR__ . '/..' . '/symfony/polyfill-php80',
),
'Symfony\\Component\\CssSelector\\' =>
array (
0 => __DIR__ . '/..' . '/symfony/css-selector',
),
'DOMWrap\\' =>
array (
0 => __DIR__ . '/..' . '/scotteh/php-dom-wrapper/src',
),
);
public static $classMap = array (
'Attribute' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/Attribute.php',
'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
'PhpToken' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/PhpToken.php',
'Stringable' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/Stringable.php',
'UnhandledMatchError' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/UnhandledMatchError.php',
'ValueError' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/ValueError.php',
);
public static function getInitializer(ClassLoader $loader)
{
return \Closure::bind(function () use ($loader) {
$loader->prefixLengthsPsr4 = ComposerStaticInit19ac79df47ecd2a96d0c835fd95d6405::$prefixLengthsPsr4;
$loader->prefixDirsPsr4 = ComposerStaticInit19ac79df47ecd2a96d0c835fd95d6405::$prefixDirsPsr4;
$loader->classMap = ComposerStaticInit19ac79df47ecd2a96d0c835fd95d6405::$classMap;
}, null, ClassLoader::class);
}
}

224
_test/vendor/composer/installed.json vendored Normal file
View File

@ -0,0 +1,224 @@
{
"packages": [
{
"name": "scotteh/php-dom-wrapper",
"version": "2.0.3",
"version_normalized": "2.0.3.0",
"source": {
"type": "git",
"url": "https://github.com/scotteh/php-dom-wrapper.git",
"reference": "edf37231a9ee609ea947ffaa5ac342a372f18b29"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/scotteh/php-dom-wrapper/zipball/edf37231a9ee609ea947ffaa5ac342a372f18b29",
"reference": "edf37231a9ee609ea947ffaa5ac342a372f18b29",
"shasum": ""
},
"require": {
"ext-libxml": "*",
"ext-mbstring": "*",
"lib-libxml": ">=2.7.7",
"php": ">=7.1.0",
"symfony/css-selector": "^4.0 || ^5.0 || ^6.0"
},
"require-dev": {
"phpunit/phpunit": "^7.0 || ^8.0 || ^9.0"
},
"time": "2022-05-14T06:10:52+00:00",
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.0-dev"
}
},
"installation-source": "dist",
"autoload": {
"psr-4": {
"DOMWrap\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Andrew Scott",
"email": "andrew@andrewscott.net.au"
}
],
"description": "Simple DOM wrapper to select nodes using either CSS or XPath expressions and manipulate results quickly and easily.",
"homepage": "https://github.com/scotteh/php-dom-wrapper",
"keywords": [
"css",
"dom",
"html",
"parser",
"wrapper"
],
"support": {
"issues": "https://github.com/scotteh/php-dom-wrapper/issues",
"source": "https://github.com/scotteh/php-dom-wrapper/tree/2.0.3"
},
"install-path": "../scotteh/php-dom-wrapper"
},
{
"name": "symfony/css-selector",
"version": "v4.4.44",
"version_normalized": "4.4.44.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/css-selector.git",
"reference": "bd0a6737e48de45b4b0b7b6fc98c78404ddceaed"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/css-selector/zipball/bd0a6737e48de45b4b0b7b6fc98c78404ddceaed",
"reference": "bd0a6737e48de45b4b0b7b6fc98c78404ddceaed",
"shasum": ""
},
"require": {
"php": ">=7.1.3",
"symfony/polyfill-php80": "^1.16"
},
"time": "2022-06-27T13:16:42+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-4": {
"Symfony\\Component\\CssSelector\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Jean-François Simon",
"email": "jeanfrancois.simon@sensiolabs.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Converts CSS selectors to XPath expressions",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/css-selector/tree/v4.4.44"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"install-path": "../symfony/css-selector"
},
{
"name": "symfony/polyfill-php80",
"version": "v1.26.0",
"version_normalized": "1.26.0.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php80.git",
"reference": "cfa0ae98841b9e461207c13ab093d76b0fa7bace"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/cfa0ae98841b9e461207c13ab093d76b0fa7bace",
"reference": "cfa0ae98841b9e461207c13ab093d76b0fa7bace",
"shasum": ""
},
"require": {
"php": ">=7.1"
},
"time": "2022-05-10T07:21:04+00:00",
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.26-dev"
},
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
}
},
"installation-source": "dist",
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Php80\\": ""
},
"classmap": [
"Resources/stubs"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Ion Bazan",
"email": "ion.bazan@gmail.com"
},
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"polyfill",
"portable",
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-php80/tree/v1.26.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"install-path": "../symfony/polyfill-php80"
}
],
"dev": true,
"dev-package-names": []
}

50
_test/vendor/composer/installed.php vendored Normal file
View File

@ -0,0 +1,50 @@
<?php return array(
'root' => array(
'pretty_version' => 'dev-master',
'version' => 'dev-master',
'type' => 'library',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
'reference' => 'ac2b3d9ee6c60ff91cda8cbd44a8de77a50a1c50',
'name' => '__root__',
'dev' => true,
),
'versions' => array(
'__root__' => array(
'pretty_version' => 'dev-master',
'version' => 'dev-master',
'type' => 'library',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
'reference' => 'ac2b3d9ee6c60ff91cda8cbd44a8de77a50a1c50',
'dev_requirement' => false,
),
'scotteh/php-dom-wrapper' => array(
'pretty_version' => '2.0.3',
'version' => '2.0.3.0',
'type' => 'library',
'install_path' => __DIR__ . '/../scotteh/php-dom-wrapper',
'aliases' => array(),
'reference' => 'edf37231a9ee609ea947ffaa5ac342a372f18b29',
'dev_requirement' => false,
),
'symfony/css-selector' => array(
'pretty_version' => 'v4.4.44',
'version' => '4.4.44.0',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/css-selector',
'aliases' => array(),
'reference' => 'bd0a6737e48de45b4b0b7b6fc98c78404ddceaed',
'dev_requirement' => false,
),
'symfony/polyfill-php80' => array(
'pretty_version' => 'v1.26.0',
'version' => '1.26.0.0',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/polyfill-php80',
'aliases' => array(),
'reference' => 'cfa0ae98841b9e461207c13ab093d76b0fa7bace',
'dev_requirement' => false,
),
),
);

View File

@ -0,0 +1,26 @@
<?php
// platform_check.php @generated by Composer
$issues = array();
if (!(PHP_VERSION_ID >= 70200)) {
$issues[] = 'Your Composer dependencies require a PHP version ">= 7.2.0". You are running ' . PHP_VERSION . '.';
}
if ($issues) {
if (!headers_sent()) {
header('HTTP/1.1 500 Internal Server Error');
}
if (!ini_get('display_errors')) {
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
fwrite(STDERR, 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . implode(PHP_EOL, $issues) . PHP_EOL.PHP_EOL);
} elseif (!headers_sent()) {
echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL;
}
}
trigger_error(
'Composer detected issues in your platform: ' . implode(' ', $issues),
E_USER_ERROR
);
}

View File

@ -0,0 +1,8 @@
version: 2
updates:
- package-ecosystem: composer
directory: "/"
schedule:
interval: weekly
time: "19:00"
open-pull-requests-limit: 10

View File

@ -0,0 +1,6 @@
composer.lock
composer.phar
phpunit.xml
vendor/
build/
.phpunit.result.cache

View File

@ -0,0 +1,28 @@
Copyright (c) 2015, Andrew Scott
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of php-dom-wrapper nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,39 @@
{
"name": "scotteh/php-dom-wrapper",
"type": "library",
"description": "Simple DOM wrapper to select nodes using either CSS or XPath expressions and manipulate results quickly and easily.",
"keywords": ["dom", "wrapper", "css", "html", "parser"],
"homepage": "https://github.com/scotteh/php-dom-wrapper",
"license": "BSD-3-Clause",
"authors": [
{
"name": "Andrew Scott",
"email": "andrew@andrewscott.net.au"
}
],
"require": {
"php": ">=7.1.0",
"ext-mbstring": "*",
"ext-libxml": "*",
"lib-libxml": ">=2.7.7",
"symfony/css-selector": "^4.0 || ^5.0 || ^6.0"
},
"require-dev": {
"phpunit/phpunit": "^7.0 || ^8.0 || ^9.0"
},
"autoload": {
"psr-4": {
"DOMWrap\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"DOMWrap\\Tests\\": "tests/"
}
},
"extra": {
"branch-alias": {
"dev-master": "2.0-dev"
}
}
}

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" bootstrap="vendor/autoload.php" colors="true" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd">
<coverage processUncoveredFiles="true">
<include>
<directory suffix=".php">src</directory>
</include>
<report>
<clover outputFile="build/logs/clover.xml"/>
<crap4j outputFile="build/logs/crap4j.xml"/>
<html outputDirectory="build/coverage"/>
<xml outputDirectory="build/logs/coverage"/>
</report>
</coverage>
<testsuites>
<testsuite name="php-dom-wrapper testsuite">
<directory>tests</directory>
</testsuite>
</testsuites>
<logging>
<junit outputFile="build/logs/junit.xml"/>
</logging>
</phpunit>

View File

@ -0,0 +1,174 @@
<?php declare(strict_types=1);
namespace DOMWrap\Collections;
/**
* Node List
*
* @package DOMWrap\Collections
* @license http://opensource.org/licenses/BSD-3-Clause BSD 3 Clause
*/
class NodeCollection implements \Countable, \ArrayAccess, \RecursiveIterator
{
/** @var array */
protected $nodes = [];
/**
* @param iterable $nodes
*/
public function __construct(iterable $nodes = null) {
if (!is_iterable($nodes)) {
$nodes = [];
}
foreach ($nodes as $node) {
$this->nodes[] = $node;
}
}
/**
* @see \Countable::count()
*
* @return int
*/
public function count(): int {
return count($this->nodes);
}
/**
* @see \ArrayAccess::offsetExists()
*
* @param mixed $offset
*
* @return bool
*/
public function offsetExists($offset): bool {
return isset($this->nodes[$offset]);
}
/**
* @see \ArrayAccess::offsetGet()
*
* @param mixed $offset
*
* @return mixed
*/
#[\ReturnTypeWillChange]
public function offsetGet($offset) {
return isset($this->nodes[$offset]) ? $this->nodes[$offset] : null;
}
/**
* @see \ArrayAccess::offsetSet()
*
* @param mixed $offset
* @param mixed $value
*/
public function offsetSet($offset, $value): void {
if (is_null($offset)) {
$this->nodes[] = $value;
} else {
$this->nodes[$offset] = $value;
}
}
/**
* @see \ArrayAccess::offsetUnset()
*
* @param mixed $offset
*/
public function offsetUnset($offset): void {
unset($this->nodes[$offset]);
}
/**
* @see \RecursiveIterator::RecursiveIteratorIterator()
*
* @return \RecursiveIteratorIterator
*/
public function getRecursiveIterator(): \RecursiveIteratorIterator {
return new \RecursiveIteratorIterator($this, \RecursiveIteratorIterator::SELF_FIRST);
}
/**
* @see \RecursiveIterator::getChildren()
*
* @return \RecursiveIterator
*/
public function getChildren(): \RecursiveIterator {
$nodes = [];
if ($this->valid()) {
$nodes = $this->current()->childNodes;
}
return new static($nodes);
}
/**
* @see \RecursiveIterator::hasChildren()
*
* @return bool
*/
public function hasChildren(): bool {
if ($this->valid()) {
return $this->current()->hasChildNodes();
}
return false;
}
/**
* @see \RecursiveIterator::current()
* @see \Iterator::current()
*
* @return mixed
*/
#[\ReturnTypeWillChange]
public function current() {
return current($this->nodes);
}
/**
* @see \RecursiveIterator::key()
* @see \Iterator::key()
*
* @return mixed
*/
#[\ReturnTypeWillChange]
public function key() {
return key($this->nodes);
}
/**
* @see \RecursiveIterator::next()
* @see \Iterator::next()
*
* @return mixed
*/
#[\ReturnTypeWillChange]
public function next() {
return next($this->nodes);
}
/**
* @see \RecursiveIterator::rewind()
* @see \Iterator::rewind()
*
* @return mixed
*/
#[\ReturnTypeWillChange]
public function rewind() {
return reset($this->nodes);
}
/**
* @see \RecursiveIterator::valid()
* @see \Iterator::valid()
*
* @return bool
*/
public function valid(): bool {
return key($this->nodes) !== null;
}
}

View File

@ -0,0 +1,24 @@
<?php declare(strict_types=1);
namespace DOMWrap;
use DOMWrap\Traits\{
NodeTrait,
CommonTrait,
TraversalTrait,
ManipulationTrait
};
/**
* Comment Node
*
* @package DOMWrap
* @license http://opensource.org/licenses/BSD-3-Clause BSD 3 Clause
*/
class Comment extends \DOMComment
{
use CommonTrait;
use NodeTrait;
use TraversalTrait;
use ManipulationTrait;
}

View File

@ -0,0 +1,271 @@
<?php declare(strict_types=1);
namespace DOMWrap;
use DOMWrap\Traits\{
CommonTrait,
TraversalTrait,
ManipulationTrait
};
/**
* Document Node
*
* @package DOMWrap
* @license http://opensource.org/licenses/BSD-3-Clause BSD 3 Clause
*/
class Document extends \DOMDocument
{
use CommonTrait;
use TraversalTrait;
use ManipulationTrait;
/** @var int */
protected $libxmlOptions = 0;
/** @var string|null */
protected $documentEncoding = null;
public function __construct(string $version = '1.0', string $encoding = 'UTF-8') {
parent::__construct($version, $encoding);
$this->registerNodeClass('DOMText', 'DOMWrap\\Text');
$this->registerNodeClass('DOMElement', 'DOMWrap\\Element');
$this->registerNodeClass('DOMComment', 'DOMWrap\\Comment');
$this->registerNodeClass('DOMDocument', 'DOMWrap\\Document');
$this->registerNodeClass('DOMDocumentType', 'DOMWrap\\DocumentType');
$this->registerNodeClass('DOMProcessingInstruction', 'DOMWrap\\ProcessingInstruction');
}
/**
* Set libxml options.
*
* Multiple values must use bitwise OR.
* eg: LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD
*
* @link http://php.net/manual/en/libxml.constants.php
*
* @param int $libxmlOptions
*/
public function setLibxmlOptions(int $libxmlOptions): void {
$this->libxmlOptions = $libxmlOptions;
}
/**
* {@inheritdoc}
*/
public function document(): ?\DOMDocument {
return $this;
}
/**
* {@inheritdoc}
*/
public function collection(): NodeList {
return $this->newNodeList([$this]);
}
/**
* {@inheritdoc}
*/
public function result(NodeList $nodeList) {
if ($nodeList->count()) {
return $nodeList->first();
}
return null;
}
/**
* {@inheritdoc}
*/
public function parent() {
return null;
}
/**
* {@inheritdoc}
*/
public function parents() {
return $this->newNodeList();
}
/**
* {@inheritdoc}
*/
public function substituteWith($newNode): self {
$this->replaceChild($newNode, $this);
return $this;
}
/**
* {@inheritdoc}
*/
public function _clone() {
return null;
}
/**
* {@inheritdoc}
*/
public function getHtml(): string {
return $this->getOuterHtml();
}
/**
* {@inheritdoc}
*/
public function setHtml($html): self {
if (!is_string($html) || trim($html) == '') {
return $this;
}
$internalErrors = libxml_use_internal_errors(true);
if (\PHP_VERSION_ID < 80000) {
$disableEntities = libxml_disable_entity_loader(true);
$this->composeXmlNode($html);
libxml_use_internal_errors($internalErrors);
libxml_disable_entity_loader($disableEntities);
} else {
$this->composeXmlNode($html);
libxml_use_internal_errors($internalErrors);
}
return $this;
}
/**
* @param string $html
* @param int $options
*
* @return bool
*/
public function loadHTML($html, $options = 0): bool {
// Fix LibXML's crazy-ness RE root nodes
// While importing HTML using the LIBXML_HTML_NOIMPLIED option LibXML insists
// on having one root node. All subsequent nodes are appended to this first node.
// To counter this we will create a fake element, allow LibXML to 'do its thing'
// then undo it by taking the contents of the fake element, placing it back into
// the root and then remove our fake element.
if ($options & LIBXML_HTML_NOIMPLIED) {
$html = '<domwrap></domwrap>' . $html;
}
$html = '<?xml encoding="' . ($this->getEncoding() ?? 'UTF-8') . '">' . $html;
$result = parent::loadHTML($html, $options);
// Do our re-shuffling of nodes.
if ($this->libxmlOptions & LIBXML_HTML_NOIMPLIED) {
$this->children()->first()->contents()->each(function($node){
$this->appendWith($node);
});
$this->removeChild($this->children()->first());
}
return $result;
}
/*
* @param $encoding string|null
*/
public function setEncoding(string $encoding = null) {
$this->documentEncoding = $encoding;
}
/*
* @return string|null
*/
public function getEncoding(): ?string {
return $this->documentEncoding;
}
/*
* @param $html string
*
* @return string|null
*/
private function getCharset(string $html): ?string {
$charset = null;
if (preg_match('@<meta.*?charset=["\']?([^"\'\s>]+)@im', $html, $matches)) {
$charset = mb_strtoupper($matches[1]);
}
return $charset;
}
/*
* @param $html string
*/
private function detectEncoding(string $html) {
$charset = $this->getEncoding();
if (is_null($charset)) {
$charset = $this->getCharset($html);
}
$detectedCharset = mb_detect_encoding($html, mb_detect_order(), true);
if ($charset === null && $detectedCharset == 'UTF-8') {
$charset = $detectedCharset;
}
$this->setEncoding($charset);
}
/*
* @param $html string
*
* @return string
*/
private function convertToUtf8(string $html): string {
$charset = $this->getEncoding();
if ($charset !== null) {
$html = preg_replace('@(charset=["]?)([^"\s]+)([^"]*["]?)@im', '$1UTF-8$3', $html);
$mbHasCharset = in_array($charset, array_map('mb_strtoupper', mb_list_encodings()));
if ($mbHasCharset) {
$html = mb_convert_encoding($html, 'UTF-8', $charset);
// Fallback to iconv if available.
} elseif (extension_loaded('iconv')) {
$htmlIconv = iconv($charset, 'UTF-8', $html);
if ($htmlIconv !== false) {
$html = $htmlIconv;
} else {
$charset = null;
}
}
}
if ($charset === null) {
$html = mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8');
}
return $html;
}
/**
* @param $html
*/
private function composeXmlNode($html)
{
$this->detectEncoding($html);
$html = $this->convertToUtf8($html);
$this->loadHTML($html, $this->libxmlOptions);
// Remove <?xml ...> processing instruction.
$this->contents()->each(function($node) {
if ($node instanceof ProcessingInstruction && $node->nodeName == 'xml') {
$node->destroy();
}
});
}
}

View File

@ -0,0 +1,24 @@
<?php declare(strict_types=1);
namespace DOMWrap;
use DOMWrap\Traits\{
NodeTrait,
CommonTrait,
TraversalTrait,
ManipulationTrait
};
/**
* DocumentType Node
*
* @package DOMWrap
* @license http://opensource.org/licenses/BSD-3-Clause BSD 3 Clause
*/
class DocumentType extends \DOMDocumentType
{
use CommonTrait;
use NodeTrait;
use TraversalTrait;
use ManipulationTrait;
}

View File

@ -0,0 +1,24 @@
<?php declare(strict_types=1);
namespace DOMWrap;
use DOMWrap\Traits\{
NodeTrait,
CommonTrait,
TraversalTrait,
ManipulationTrait
};
/**
* Element Node
*
* @package DOMWrap
* @license http://opensource.org/licenses/BSD-3-Clause BSD 3 Clause
*/
class Element extends \DOMElement
{
use CommonTrait;
use NodeTrait;
use TraversalTrait;
use ManipulationTrait;
}

View File

@ -0,0 +1,292 @@
<?php declare(strict_types=1);
namespace DOMWrap;
use DOMWrap\Traits\{
CommonTrait,
TraversalTrait,
ManipulationTrait
};
use DOMWrap\Collections\NodeCollection;
/**
* Node List
*
* @package DOMWrap
* @license http://opensource.org/licenses/BSD-3-Clause BSD 3 Clause
*/
class NodeList extends NodeCollection
{
use CommonTrait;
use TraversalTrait;
use ManipulationTrait {
ManipulationTrait::__call as __manipulationCall;
}
/** @var Document */
protected $document;
/**
* @param Document $document
* @param iterable $nodes
*/
public function __construct(Document $document = null, iterable $nodes = null) {
parent::__construct($nodes);
$this->document = $document;
}
/**
* @param string $name
* @param array $arguments
*
* @return mixed
*/
public function __call(string $name, array $arguments) {
try {
$result = $this->__manipulationCall($name, $arguments);
} catch (\BadMethodCallException $e) {
if (!$this->first() || !method_exists($this->first(), $name)) {
throw new \BadMethodCallException("Call to undefined method " . get_class($this) . '::' . $name . "()");
}
$result = call_user_func_array([$this->first(), $name], $arguments);
}
return $result;
}
/**
* {@inheritdoc}
*/
public function collection(): NodeList {
return $this;
}
/**
* {@inheritdoc}
*/
public function document(): ?\DOMDocument {
return $this->document;
}
/**
* {@inheritdoc}
*/
public function result(NodeList $nodeList) {
return $nodeList;
}
/**
* @return NodeList
*/
public function reverse(): NodeList {
array_reverse($this->nodes);
return $this;
}
/**
* @return mixed
*/
public function first() {
return !empty($this->nodes) ? $this->rewind() : null;
}
/**
* @return mixed
*/
public function last() {
return $this->end();
}
/**
* @return mixed
*/
public function end() {
return !empty($this->nodes) ? end($this->nodes) : null;
}
/**
* @param int $key
*
* @return mixed
*/
public function get(int $key) {
if (isset($this->nodes[$key])) {
return $this->nodes[$key];
}
return null;
}
/**
* @param int $key
* @param mixed $value
*
* @return self
*/
public function set(int $key, $value): self {
$this->nodes[$key] = $value;
return $this;
}
/**
* @param callable $function
*
* @return self
*/
public function each(callable $function): self {
foreach ($this->nodes as $index => $node) {
$result = $function($node, $index);
if ($result === false) {
break;
}
}
return $this;
}
/**
* @param callable $function
*
* @return NodeList
*/
public function map(callable $function): NodeList {
$nodes = $this->newNodeList();
foreach ($this->nodes as $node) {
$result = $function($node);
if (!is_null($result) && $result !== false) {
$nodes[] = $result;
}
}
return $nodes;
}
/**
* @param callable $function
* @param mixed|null $initial
*
* @return iterable
*/
public function reduce(callable $function, $initial = null) {
return array_reduce($this->nodes, $function, $initial);
}
/**
* @return array
*/
public function toArray() {
return $this->nodes;
}
/**
* @param iterable $nodes
*/
public function fromArray(iterable $nodes = null) {
$this->nodes = [];
if (is_iterable($nodes)) {
foreach ($nodes as $node) {
$this->nodes[] = $node;
}
}
}
/**
* @param NodeList|array $elements
*
* @return NodeList
*/
public function merge($elements = []): NodeList {
if (!is_array($elements)) {
$elements = $elements->toArray();
}
return $this->newNodeList(array_merge($this->toArray(), $elements));
}
/**
* @param int $start
* @param int $end
*
* @return NodeList
*/
public function slice(int $start, int $end = null): NodeList {
$nodeList = array_slice($this->toArray(), $start, $end);
return $this->newNodeList($nodeList);
}
/**
* @param \DOMNode $node
*
* @return self
*/
public function push(\DOMNode $node): self {
$this->nodes[] = $node;
return $this;
}
/**
* @return \DOMNode
*/
public function pop(): \DOMNode {
return array_pop($this->nodes);
}
/**
* @param \DOMNode $node
*
* @return self
*/
public function unshift(\DOMNode $node): self {
array_unshift($this->nodes, $node);
return $this;
}
/**
* @return \DOMNode
*/
public function shift(): \DOMNode {
return array_shift($this->nodes);
}
/**
* @param \DOMNode $node
*
* @return bool
*/
public function exists(\DOMNode $node): bool {
return in_array($node, $this->nodes, true);
}
/**
* @param \DOMNode $node
*
* @return self
*/
public function delete(\DOMNode $node): self {
$index = array_search($node, $this->nodes, true);
if ($index !== false) {
unset($this->nodes[$index]);
}
return $this;
}
/**
* @return bool
*/
public function isRemoved(): bool {
return false;
}
}

View File

@ -0,0 +1,24 @@
<?php declare(strict_types=1);
namespace DOMWrap;
use DOMWrap\Traits\{
NodeTrait,
CommonTrait,
TraversalTrait,
ManipulationTrait
};
/**
* ProcessingInstruction Node
*
* @package DOMWrap
* @license http://opensource.org/licenses/BSD-3-Clause BSD 3 Clause
*/
class ProcessingInstruction extends \DOMProcessingInstruction
{
use CommonTrait;
use NodeTrait;
use TraversalTrait;
use ManipulationTrait;
}

View File

@ -0,0 +1,24 @@
<?php declare(strict_types=1);
namespace DOMWrap;
use DOMWrap\Traits\{
NodeTrait,
CommonTrait,
TraversalTrait,
ManipulationTrait
};
/**
* Text Node
*
* @package DOMWrap
* @license http://opensource.org/licenses/BSD-3-Clause BSD 3 Clause
*/
class Text extends \DOMText
{
use CommonTrait;
use NodeTrait;
use TraversalTrait;
use ManipulationTrait;
}

View File

@ -0,0 +1,38 @@
<?php declare(strict_types=1);
namespace DOMWrap\Traits;
use DOMWrap\NodeList;
/**
* Common Trait
*
* @package DOMWrap\Traits
* @license http://opensource.org/licenses/BSD-3-Clause BSD 3 Clause
*/
trait CommonTrait
{
/**
* @return NodeList
*/
abstract public function collection(): NodeList;
/**
* @return \DOMDocument
*/
abstract public function document(): ?\DOMDocument;
/**
* @param NodeList $nodeList
*
* @return NodeList|\DOMNode
*/
abstract public function result(NodeList $nodeList);
/**
* @return bool
*/
public function isRemoved(): bool {
return !isset($this->nodeType);
}
}

View File

@ -0,0 +1,748 @@
<?php declare(strict_types=1);
namespace DOMWrap\Traits;
use DOMWrap\{
Text,
Element,
NodeList
};
/**
* Manipulation Trait
*
* @package DOMWrap\Traits
* @license http://opensource.org/licenses/BSD-3-Clause BSD 3 Clause
*/
trait ManipulationTrait
{
/**
* Magic method - Trap function names using reserved keyword (empty, clone, etc..)
*
* @param string $name
* @param array $arguments
*
* @return mixed
*/
public function __call(string $name, array $arguments) {
if (!method_exists($this, '_' . $name)) {
throw new \BadMethodCallException("Call to undefined method " . get_class($this) . '::' . $name . "()");
}
return call_user_func_array([$this, '_' . $name], $arguments);
}
/**
* @return string
*/
public function __toString(): string {
return $this->getOuterHtml(true);
}
/**
* @param string|NodeList|\DOMNode $input
*
* @return iterable
*/
protected function inputPrepareAsTraversable($input): iterable {
if ($input instanceof \DOMNode) {
// Handle raw \DOMNode elements and 'convert' them into their DOMWrap/* counterpart
if (!method_exists($input, 'inputPrepareAsTraversable')) {
$input = $this->document()->importNode($input, true);
}
$nodes = [$input];
} else if (is_string($input)) {
$nodes = $this->nodesFromHtml($input);
} else if (is_iterable($input)) {
$nodes = $input;
} else {
throw new \InvalidArgumentException();
}
return $nodes;
}
/**
* @param string|NodeList|\DOMNode $input
* @param bool $cloneForManipulate
*
* @return NodeList
*/
protected function inputAsNodeList($input, $cloneForManipulate = true): NodeList {
$nodes = $this->inputPrepareAsTraversable($input);
$newNodes = $this->newNodeList();
foreach ($nodes as $node) {
if ($node->document() !== $this->document()) {
$node = $this->document()->importNode($node, true);
}
if ($cloneForManipulate && $node->parentNode !== null) {
$node = $node->cloneNode(true);
}
$newNodes[] = $node;
}
return $newNodes;
}
/**
* @param string|NodeList|\DOMNode $input
*
* @return \DOMNode|null
*/
protected function inputAsFirstNode($input): ?\DOMNode {
$nodes = $this->inputAsNodeList($input);
return $nodes->findXPath('self::*')->first();
}
/**
* @param string $html
*
* @return NodeList
*/
protected function nodesFromHtml($html): NodeList {
$class = get_class($this->document());
$doc = new $class();
$doc->setEncoding($this->document()->getEncoding());
$nodes = $doc->html($html)->find('body > *');
return $nodes;
}
/**
* @param string|NodeList|\DOMNode|callable $input
* @param callable $callback
*
* @return self
*/
protected function manipulateNodesWithInput($input, callable $callback): self {
$this->collection()->each(function($node, $index) use ($input, $callback) {
$html = $input;
/*if ($input instanceof \DOMNode) {
if ($input->parentNode !== null) {
$html = $input->cloneNode(true);
}
} else*/if (is_callable($input)) {
$html = $input($node, $index);
}
$newNodes = $this->inputAsNodeList($html);
$callback($node, $newNodes);
});
return $this;
}
/**
* @param string|null $selector
*
* @return NodeList
*/
public function detach(string $selector = null): NodeList {
if (!is_null($selector)) {
$nodes = $this->find($selector, 'self::');
} else {
$nodes = $this->collection();
}
$nodeList = $this->newNodeList();
$nodes->each(function($node) use($nodeList) {
if ($node->parent() instanceof \DOMNode) {
$nodeList[] = $node->parent()->removeChild($node);
}
});
$nodes->fromArray([]);
return $nodeList;
}
/**
* @param string|null $selector
*
* @return self
*/
public function destroy(string $selector = null): self {
$this->detach($selector);
return $this;
}
/**
* @param string|NodeList|\DOMNode|callable $input
*
* @return self
*/
public function substituteWith($input): self {
$this->manipulateNodesWithInput($input, function($node, $newNodes) {
foreach ($newNodes as $newNode) {
$node->parent()->replaceChild($newNode, $node);
}
});
return $this;
}
/**
* @param string|NodeList|\DOMNode|callable $input
*
* @return string|self
*/
public function text($input = null) {
if (is_null($input)) {
return $this->getText();
} else {
return $this->setText($input);
}
}
/**
* @return string
*/
public function getText(): string {
return (string)$this->collection()->reduce(function($carry, $node) {
return $carry . $node->textContent;
}, '');
}
/**
* @param string|NodeList|\DOMNode|callable $input
*
* @return self
*/
public function setText($input): self {
if (is_string($input)) {
$input = new Text($input);
}
$this->manipulateNodesWithInput($input, function($node, $newNodes) {
// Remove old contents from the current node.
$node->contents()->destroy();
// Add new contents in it's place.
$node->appendWith(new Text($newNodes->getText()));
});
return $this;
}
/**
* @param string|NodeList|\DOMNode|callable $input
*
* @return self
*/
public function precede($input): self {
$this->manipulateNodesWithInput($input, function($node, $newNodes) {
foreach ($newNodes as $newNode) {
$node->parent()->insertBefore($newNode, $node);
}
});
return $this;
}
/**
* @param string|NodeList|\DOMNode|callable $input
*
* @return self
*/
public function follow($input): self {
$this->manipulateNodesWithInput($input, function($node, $newNodes) {
foreach ($newNodes as $newNode) {
if (is_null($node->following())) {
$node->parent()->appendChild($newNode);
} else {
$node->parent()->insertBefore($newNode, $node->following());
}
}
});
return $this;
}
/**
* @param string|NodeList|\DOMNode|callable $input
*
* @return self
*/
public function prependWith($input): self {
$this->manipulateNodesWithInput($input, function($node, $newNodes) {
foreach ($newNodes as $newNode) {
$node->insertBefore($newNode, $node->contents()->first());
}
});
return $this;
}
/**
* @param string|NodeList|\DOMNode|callable $input
*
* @return self
*/
public function appendWith($input): self {
$this->manipulateNodesWithInput($input, function($node, $newNodes) {
foreach ($newNodes as $newNode) {
$node->appendChild($newNode);
}
});
return $this;
}
/**
* @param string|NodeList|\DOMNode $selector
*
* @return self
*/
public function prependTo($selector): self {
if ($selector instanceof \DOMNode || $selector instanceof NodeList) {
$nodes = $this->inputAsNodeList($selector);
} else {
$nodes = $this->document()->find($selector);
}
$nodes->prependWith($this);
return $this;
}
/**
* @param string|NodeList|\DOMNode $selector
*
* @return self
*/
public function appendTo($selector): self {
if ($selector instanceof \DOMNode || $selector instanceof NodeList) {
$nodes = $this->inputAsNodeList($selector);
} else {
$nodes = $this->document()->find($selector);
}
$nodes->appendWith($this);
return $this;
}
/**
* @return self
*/
public function _empty(): self {
$this->collection()->each(function($node) {
$node->contents()->destroy();
});
return $this;
}
/**
* @return NodeList|\DOMNode
*/
public function _clone() {
$clonedNodes = $this->newNodeList();
$this->collection()->each(function($node) use($clonedNodes) {
$clonedNodes[] = $node->cloneNode(true);
});
return $this->result($clonedNodes);
}
/**
* @param string $name
*
* @return self
*/
public function removeAttr(string $name): self {
$this->collection()->each(function($node) use($name) {
if ($node instanceof \DOMElement) {
$node->removeAttribute($name);
}
});
return $this;
}
/**
* @param string $name
*
* @return bool
*/
public function hasAttr(string $name): bool {
return (bool)$this->collection()->reduce(function($carry, $node) use ($name) {
if ($node->hasAttribute($name)) {
return true;
}
return $carry;
}, false);
}
/**
* @internal
*
* @param string $name
*
* @return string
*/
public function getAttr(string $name): string {
$node = $this->collection()->first();
if (!($node instanceof \DOMElement)) {
return '';
}
return $node->getAttribute($name);
}
/**
* @internal
*
* @param string $name
* @param mixed $value
*
* @return self
*/
public function setAttr(string $name, $value): self {
$this->collection()->each(function($node) use($name, $value) {
if ($node instanceof \DOMElement) {
$node->setAttribute($name, (string)$value);
}
});
return $this;
}
/**
* @param string $name
* @param mixed $value
*
* @return self|string
*/
public function attr(string $name, $value = null) {
if (is_null($value)) {
return $this->getAttr($name);
} else {
return $this->setAttr($name, $value);
}
}
/**
* @internal
*
* @param string $name
* @param string|callable $value
* @param bool $addValue
*/
protected function _pushAttrValue(string $name, $value, bool $addValue = false): void {
$this->collection()->each(function($node, $index) use($name, $value, $addValue) {
if ($node instanceof \DOMElement) {
$attr = $node->getAttribute($name);
if (is_callable($value)) {
$value = $value($node, $index, $attr);
}
// Remove any existing instances of the value, or empty values.
$values = array_filter(explode(' ', $attr), function($_value) use($value) {
if (strcasecmp($_value, $value) == 0 || empty($_value)) {
return false;
}
return true;
});
// If required add attr value to array
if ($addValue) {
$values[] = $value;
}
// Set the attr if we either have values, or the attr already
// existed (we might be removing classes).
//
// Don't set the attr if it doesn't already exist.
if (!empty($values) || $node->hasAttribute($name)) {
$node->setAttribute($name, implode(' ', $values));
}
}
});
}
/**
* @param string|callable $class
*
* @return self
*/
public function addClass($class): self {
$this->_pushAttrValue('class', $class, true);
return $this;
}
/**
* @param string|callable $class
*
* @return self
*/
public function removeClass($class): self {
$this->_pushAttrValue('class', $class);
return $this;
}
/**
* @param string $class
*
* @return bool
*/
public function hasClass(string $class): bool {
return (bool)$this->collection()->reduce(function($carry, $node) use ($class) {
$attr = $node->getAttr('class');
return array_reduce(explode(' ', (string)$attr), function($carry, $item) use ($class) {
if (strcasecmp($item, $class) == 0) {
return true;
}
return $carry;
}, false);
}, false);
}
/**
* @param Element $node
*
* @return \SplStack
*/
protected function _getFirstChildWrapStack(Element $node): \SplStack {
$stack = new \SplStack;
do {
// Push our current node onto the stack
$stack->push($node);
// Get the first element child node
$node = $node->children()->first();
} while ($node instanceof Element);
// Get the top most node.
return $stack;
}
/**
* @param Element $node
*
* @return \SplStack
*/
protected function _prepareWrapStack(Element $node): \SplStack {
// Generate a stack (root to leaf) of the wrapper.
// Includes only first element nodes / first element children.
$stackNodes = $this->_getFirstChildWrapStack($node);
// Only using the first element, remove any siblings.
foreach ($stackNodes as $stackNode) {
$stackNode->siblings()->destroy();
}
return $stackNodes;
}
/**
* @param string|NodeList|\DOMNode|callable $input
* @param callable $callback
*/
protected function wrapWithInputByCallback($input, callable $callback): void {
$this->collection()->each(function($node, $index) use ($input, $callback) {
$html = $input;
if (is_callable($input)) {
$html = $input($node, $index);
}
$inputNode = $this->inputAsFirstNode($html);
if ($inputNode instanceof Element) {
// Pre-process wrapper into a stack of first element nodes.
$stackNodes = $this->_prepareWrapStack($inputNode);
$callback($node, $stackNodes);
}
});
}
/**
* @param string|NodeList|\DOMNode|callable $input
*
* @return self
*/
public function wrapInner($input): self {
$this->wrapWithInputByCallback($input, function($node, $stackNodes) {
foreach ($node->contents() as $child) {
// Remove child from the current node
$oldChild = $child->detach()->first();
// Add it back as a child of the top (leaf) node on the stack
$stackNodes->top()->appendWith($oldChild);
}
// Add the bottom (root) node on the stack
$node->appendWith($stackNodes->bottom());
});
return $this;
}
/**
* @param string|NodeList|\DOMNode|callable $input
*
* @return self
*/
public function wrap($input): self {
$this->wrapWithInputByCallback($input, function($node, $stackNodes) {
// Add the new bottom (root) node after the current node
$node->follow($stackNodes->bottom());
// Remove the current node
$oldNode = $node->detach()->first();
// Add the 'current node' back inside the new top (leaf) node.
$stackNodes->top()->appendWith($oldNode);
});
return $this;
}
/**
* @param string|NodeList|\DOMNode|callable $input
*
* @return self
*/
public function wrapAll($input): self {
if (!$this->collection()->count()) {
return $this;
}
if (is_callable($input)) {
$input = $input($this->collection()->first());
}
$inputNode = $this->inputAsFirstNode($input);
if (!($inputNode instanceof Element)) {
return $this;
}
$stackNodes = $this->_prepareWrapStack($inputNode);
// Add the new bottom (root) node before the first matched node
$this->collection()->first()->precede($stackNodes->bottom());
$this->collection()->each(function($node) use ($stackNodes) {
// Detach and add node back inside the new wrappers top (leaf) node.
$stackNodes->top()->appendWith($node->detach());
});
return $this;
}
/**
* @return self
*/
public function unwrap(): self {
$this->collection()->each(function($node) {
$parent = $node->parent();
// Replace parent node (the one we're unwrapping) with it's children.
$parent->contents()->each(function($childNode) use($parent) {
$oldChildNode = $childNode->detach()->first();
$parent->precede($oldChildNode);
});
$parent->destroy();
});
return $this;
}
/**
* @param int $isIncludeAll
*
* @return string
*/
public function getOuterHtml(bool $isIncludeAll = false): string {
$nodes = $this->collection();
if (!$isIncludeAll) {
$nodes = $this->newNodeList([$nodes->first()]);
}
return $nodes->reduce(function($carry, $node) {
return $carry . $this->document()->saveHTML($node);
}, '');
}
/**
* @param int $isIncludeAll
*
* @return string
*/
public function getHtml(bool $isIncludeAll = false): string {
$nodes = $this->collection();
if (!$isIncludeAll) {
$nodes = $this->newNodeList([$nodes->first()]);
}
return $nodes->contents()->reduce(function($carry, $node) {
return $carry . $this->document()->saveHTML($node);
}, '');
}
/**
* @param string|NodeList|\DOMNode|callable $input
*
* @return self
*/
public function setHtml($input): self {
$this->manipulateNodesWithInput($input, function($node, $newNodes) {
// Remove old contents from the current node.
$node->contents()->destroy();
// Add new contents in it's place.
$node->appendWith($newNodes);
});
return $this;
}
/**
* @param string|NodeList|\DOMNode|callable $input
*
* @return string|self
*/
public function html($input = null) {
if (is_null($input)) {
return $this->getHtml();
} else {
return $this->setHtml($input);
}
}
/**
* @param string|NodeList|\DOMNode $input
*
* @return NodeList
*/
public function create($input): NodeList {
return $this->inputAsNodeList($input);
}
}

View File

@ -0,0 +1,46 @@
<?php declare(strict_types=1);
namespace DOMWrap\Traits;
use DOMWrap\NodeList;
/**
* Node Trait
*
* @package DOMWrap\Traits
* @license http://opensource.org/licenses/BSD-3-Clause BSD 3 Clause
* @property \DOMDocument $ownerDocument
*/
trait NodeTrait
{
/**
* @return NodeList
*/
public function collection(): NodeList {
return $this->newNodeList([$this]);
}
/**
* @return \DOMDocument
*/
public function document(): ?\DOMDocument {
if ($this->isRemoved()) {
return null;
}
return $this->ownerDocument;
}
/**
* @param NodeList $nodeList
*
* @return NodeList|\DOMNode|null
*/
public function result(NodeList $nodeList) {
if ($nodeList->count()) {
return $nodeList->first();
}
return null;
}
}

View File

@ -0,0 +1,463 @@
<?php declare(strict_types=1);
namespace DOMWrap\Traits;
use DOMWrap\{
Element,
NodeList
};
use Symfony\Component\CssSelector\CssSelectorConverter;
/**
* Traversal Trait
*
* @package DOMWrap\Traits
* @license http://opensource.org/licenses/BSD-3-Clause BSD 3 Clause
*/
trait TraversalTrait
{
/**
* @param iterable $nodes
*
* @return NodeList
*/
public function newNodeList(iterable $nodes = null): NodeList {
if (!is_iterable($nodes)) {
if (!is_null($nodes)) {
$nodes = [$nodes];
} else {
$nodes = [];
}
}
return new NodeList($this->document(), $nodes);
}
/**
* @param string $selector
* @param string $prefix
*
* @return NodeList
*/
public function find(string $selector, string $prefix = 'descendant::'): NodeList {
$converter = new CssSelectorConverter();
return $this->findXPath($converter->toXPath($selector, $prefix));
}
/**
* @param string $xpath
*
* @return NodeList
*/
public function findXPath(string $xpath): NodeList {
$results = $this->newNodeList();
if ($this->isRemoved()) {
return $results;
}
$domxpath = new \DOMXPath($this->document());
foreach ($this->collection() as $node) {
$results = $results->merge(
$node->newNodeList($domxpath->query($xpath, $node))
);
}
return $results;
}
/**
* @param string|NodeList|\DOMNode|callable $input
* @param bool $matchType
*
* @return NodeList
*/
protected function getNodesMatchingInput($input, bool $matchType = true): NodeList {
if ($input instanceof NodeList || $input instanceof \DOMNode) {
$inputNodes = $this->inputAsNodeList($input, false);
$fn = function($node) use ($inputNodes) {
return $inputNodes->exists($node);
};
} elseif (is_callable($input)) {
// Since we're at the behest of the input callable, the 'matched'
// return value is always true.
$matchType = true;
$fn = $input;
} elseif (is_string($input)) {
$fn = function($node) use ($input) {
return $node->find($input, 'self::')->count() != 0;
};
} else {
throw new \InvalidArgumentException('Unexpected input value of type "' . gettype($input) . '"');
}
// Build a list of matching nodes.
return $this->collection()->map(function($node) use ($fn, $matchType) {
if ($fn($node) !== $matchType) {
return null;
}
return $node;
});
}
/**
* @param string|NodeList|\DOMNode|callable $input
*
* @return bool
*/
public function is($input): bool {
return $this->getNodesMatchingInput($input)->count() != 0;
}
/**
* @param string|NodeList|\DOMNode|callable $input
*
* @return NodeList
*/
public function not($input): NodeList {
return $this->getNodesMatchingInput($input, false);
}
/**
* @param string|NodeList|\DOMNode|callable $input
*
* @return NodeList
*/
public function filter($input): NodeList {
return $this->getNodesMatchingInput($input);
}
/**
* @param string|NodeList|\DOMNode|callable $input
*
* @return NodeList
*/
public function has($input): NodeList {
if ($input instanceof NodeList || $input instanceof \DOMNode) {
$inputNodes = $this->inputAsNodeList($input, false);
$fn = function($node) use ($inputNodes) {
$descendantNodes = $node->find('*', 'descendant::');
// Determine if we have a descendant match.
return $inputNodes->reduce(function($carry, $inputNode) use ($descendantNodes) {
// Match descendant nodes against input nodes.
if ($descendantNodes->exists($inputNode)) {
return true;
}
return $carry;
}, false);
};
} elseif (is_string($input)) {
$fn = function($node) use ($input) {
return $node->find($input, 'descendant::')->count() != 0;
};
} elseif (is_callable($input)) {
$fn = $input;
} else {
throw new \InvalidArgumentException('Unexpected input value of type "' . gettype($input) . '"');
}
return $this->getNodesMatchingInput($fn);
}
/**
* @param string|NodeList|\DOMNode|callable $selector
*
* @return \DOMNode|null
*/
public function preceding($selector = null): ?\DOMNode {
return $this->precedingUntil(null, $selector)->first();
}
/**
* @param string|NodeList|\DOMNode|callable $selector
*
* @return NodeList
*/
public function precedingAll($selector = null): NodeList {
return $this->precedingUntil(null, $selector);
}
/**
* @param string|NodeList|\DOMNode|callable $input
* @param string|NodeList|\DOMNode|callable $selector
*
* @return NodeList
*/
public function precedingUntil($input = null, $selector = null): NodeList {
return $this->_walkPathUntil('previousSibling', $input, $selector);
}
/**
* @param string|NodeList|\DOMNode|callable $selector
*
* @return \DOMNode|null
*/
public function following($selector = null): ?\DOMNode {
return $this->followingUntil(null, $selector)->first();
}
/**
* @param string|NodeList|\DOMNode|callable $selector
*
* @return NodeList
*/
public function followingAll($selector = null): NodeList {
return $this->followingUntil(null, $selector);
}
/**
* @param string|NodeList|\DOMNode|callable $input
* @param string|NodeList|\DOMNode|callable $selector
*
* @return NodeList
*/
public function followingUntil($input = null, $selector = null): NodeList {
return $this->_walkPathUntil('nextSibling', $input, $selector);
}
/**
* @param string|NodeList|\DOMNode|callable $selector
*
* @return NodeList
*/
public function siblings($selector = null): NodeList {
$results = $this->collection()->reduce(function($carry, $node) use ($selector) {
return $carry->merge(
$node->precedingAll($selector)->merge(
$node->followingAll($selector)
)
);
}, $this->newNodeList());
return $results;
}
/**
* NodeList is only array like. Removing items using foreach() has undesired results.
*
* @return NodeList
*/
public function children(): NodeList {
$results = $this->collection()->reduce(function($carry, $node) {
return $carry->merge(
$node->findXPath('child::*')
);
}, $this->newNodeList());
return $results;
}
/**
* @param string|NodeList|\DOMNode|callable $selector
*
* @return Element|NodeList|null
*/
public function parent($selector = null) {
$results = $this->_walkPathUntil('parentNode', null, $selector, self::$MATCH_TYPE_FIRST);
return $this->result($results);
}
/**
* @param int $index
*
* @return \DOMNode|null
*/
public function eq(int $index): ?\DOMNode {
if ($index < 0) {
$index = $this->collection()->count() + $index;
}
return $this->collection()->offsetGet($index);
}
/**
* @param string $selector
*
* @return NodeList
*/
public function parents(string $selector = null): NodeList {
return $this->parentsUntil(null, $selector);
}
/**
* @param string|NodeList|\DOMNode|callable $input
* @param string|NodeList|\DOMNode|callable $selector
*
* @return NodeList
*/
public function parentsUntil($input = null, $selector = null): NodeList {
return $this->_walkPathUntil('parentNode', $input, $selector);
}
/**
* @return \DOMNode
*/
public function intersect(): \DOMNode {
if ($this->collection()->count() < 2) {
return $this->collection()->first();
}
$nodeParents = [];
// Build a multi-dimensional array of the collection nodes parent elements
$this->collection()->each(function($node) use(&$nodeParents) {
$nodeParents[] = $node->parents()->unshift($node)->toArray();
});
// Find the common parent
$diff = call_user_func_array('array_uintersect', array_merge($nodeParents, [function($a, $b) {
return strcmp(spl_object_hash($a), spl_object_hash($b));
}]));
return array_shift($diff);
}
/**
* @param string|NodeList|\DOMNode|callable $input
*
* @return Element|NodeList|null
*/
public function closest($input) {
$results = $this->_walkPathUntil('parentNode', $input, null, self::$MATCH_TYPE_LAST);
return $this->result($results);
}
/**
* NodeList is only array like. Removing items using foreach() has undesired results.
*
* @return NodeList
*/
public function contents(): NodeList {
$results = $this->collection()->reduce(function($carry, $node) {
if ($node->isRemoved()) {
return $carry;
}
return $carry->merge(
$node->newNodeList($node->childNodes)
);
}, $this->newNodeList());
return $results;
}
/**
* @param string|NodeList|\DOMNode $input
*
* @return NodeList
*/
public function add($input): NodeList {
$nodes = $this->inputAsNodeList($input);
$results = $this->collection()->merge(
$nodes
);
return $results;
}
/** @var int */
private static $MATCH_TYPE_FIRST = 1;
/** @var int */
private static $MATCH_TYPE_LAST = 2;
/**
* @param \DOMNode $baseNode
* @param string $property
* @param string|NodeList|\DOMNode|callable $input
* @param string|NodeList|\DOMNode|callable $selector
* @param int $matchType
*
* @return NodeList
*/
protected function _buildNodeListUntil(\DOMNode $baseNode, string $property, $input = null, $selector = null, int $matchType = null): NodeList {
$resultNodes = $this->newNodeList();
// Get our first node
$node = $baseNode->$property;
// Keep looping until we are out of nodes.
// Allow either FIRST to reach \DOMDocument. Others that return multiple should ignore it.
while ($node instanceof \DOMNode && ($matchType === self::$MATCH_TYPE_FIRST || !($node instanceof \DOMDocument))) {
// Filter nodes if not matching last
if ($matchType != self::$MATCH_TYPE_LAST && (is_null($selector) || $node->is($selector))) {
$resultNodes[] = $node;
}
// 'Until' check or first match only
if ($matchType == self::$MATCH_TYPE_FIRST || (!is_null($input) && $node->is($input))) {
// Set last match
if ($matchType == self::$MATCH_TYPE_LAST) {
$resultNodes[] = $node;
}
break;
}
// Find the next node
$node = $node->{$property};
}
return $resultNodes;
}
/**
* @param iterable $nodeLists
*
* @return NodeList
*/
protected function _uniqueNodes(iterable $nodeLists): NodeList {
$resultNodes = $this->newNodeList();
// Loop through our array of NodeLists
foreach ($nodeLists as $nodeList) {
// Each node in the NodeList
foreach ($nodeList as $node) {
// We're only interested in unique nodes
if (!$resultNodes->exists($node)) {
$resultNodes[] = $node;
}
}
}
// Sort resulting NodeList: outer-most => inner-most.
return $resultNodes->reverse();
}
/**
* @param string $property
* @param string|NodeList|\DOMNode|callable $input
* @param string|NodeList|\DOMNode|callable $selector
* @param int $matchType
*
* @return NodeList
*/
protected function _walkPathUntil(string $property, $input = null, $selector = null, int $matchType = null): NodeList {
$nodeLists = [];
$this->collection()->each(function($node) use($property, $input, $selector, $matchType, &$nodeLists) {
$nodeLists[] = $this->_buildNodeListUntil($node, $property, $input, $selector, $matchType);
});
return $this->_uniqueNodes($nodeLists);
}
}

View File

@ -0,0 +1,18 @@
CHANGELOG
=========
4.4.0
-----
* Added support for `*:only-of-type`
2.8.0
-----
* Added the `CssSelectorConverter` class as a non-static API for the component.
* Deprecated the `CssSelector` static API of the component.
2.1.0
-----
* none

View File

@ -0,0 +1,65 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector;
use Symfony\Component\CssSelector\Parser\Shortcut\ClassParser;
use Symfony\Component\CssSelector\Parser\Shortcut\ElementParser;
use Symfony\Component\CssSelector\Parser\Shortcut\EmptyStringParser;
use Symfony\Component\CssSelector\Parser\Shortcut\HashParser;
use Symfony\Component\CssSelector\XPath\Extension\HtmlExtension;
use Symfony\Component\CssSelector\XPath\Translator;
/**
* CssSelectorConverter is the main entry point of the component and can convert CSS
* selectors to XPath expressions.
*
* @author Christophe Coevoet <stof@notk.org>
*/
class CssSelectorConverter
{
private $translator;
/**
* @param bool $html Whether HTML support should be enabled. Disable it for XML documents
*/
public function __construct(bool $html = true)
{
$this->translator = new Translator();
if ($html) {
$this->translator->registerExtension(new HtmlExtension($this->translator));
}
$this->translator
->registerParserShortcut(new EmptyStringParser())
->registerParserShortcut(new ElementParser())
->registerParserShortcut(new ClassParser())
->registerParserShortcut(new HashParser())
;
}
/**
* Translates a CSS expression to its XPath equivalent.
*
* Optionally, a prefix can be added to the resulting XPath
* expression with the $prefix parameter.
*
* @param string $cssExpr The CSS expression
* @param string $prefix An optional prefix for the XPath expression
*
* @return string
*/
public function toXPath($cssExpr, $prefix = 'descendant-or-self::')
{
return $this->translator->cssToXPath($cssExpr, $prefix);
}
}

View File

@ -0,0 +1,24 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\Exception;
/**
* Interface for exceptions.
*
* This component is a port of the Python cssselect library,
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
*
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
*/
interface ExceptionInterface extends \Throwable
{
}

View File

@ -0,0 +1,24 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\Exception;
/**
* ParseException is thrown when a CSS selector syntax is not valid.
*
* This component is a port of the Python cssselect library,
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
*
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
*/
class ExpressionErrorException extends ParseException
{
}

View File

@ -0,0 +1,24 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\Exception;
/**
* ParseException is thrown when a CSS selector syntax is not valid.
*
* This component is a port of the Python cssselect library,
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
*
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
*/
class InternalErrorException extends ParseException
{
}

View File

@ -0,0 +1,24 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\Exception;
/**
* ParseException is thrown when a CSS selector syntax is not valid.
*
* This component is a port of the Python cssselect library,
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class ParseException extends \Exception implements ExceptionInterface
{
}

View File

@ -0,0 +1,72 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\Exception;
use Symfony\Component\CssSelector\Parser\Token;
/**
* ParseException is thrown when a CSS selector syntax is not valid.
*
* This component is a port of the Python cssselect library,
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
*
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
*/
class SyntaxErrorException extends ParseException
{
/**
* @param string $expectedValue
*
* @return self
*/
public static function unexpectedToken($expectedValue, Token $foundToken)
{
return new self(sprintf('Expected %s, but %s found.', $expectedValue, $foundToken));
}
/**
* @param string $pseudoElement
* @param string $unexpectedLocation
*
* @return self
*/
public static function pseudoElementFound($pseudoElement, $unexpectedLocation)
{
return new self(sprintf('Unexpected pseudo-element "::%s" found %s.', $pseudoElement, $unexpectedLocation));
}
/**
* @param int $position
*
* @return self
*/
public static function unclosedString($position)
{
return new self(sprintf('Unclosed/invalid string at %s.', $position));
}
/**
* @return self
*/
public static function nestedNot()
{
return new self('Got nested ::not().');
}
/**
* @return self
*/
public static function stringAsFunctionArgument()
{
return new self('String not allowed as function argument.');
}
}

View File

@ -0,0 +1,19 @@
Copyright (c) 2004-2022 Fabien Potencier
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@ -0,0 +1,39 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\Node;
/**
* Abstract base node class.
*
* This component is a port of the Python cssselect library,
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
*
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
*
* @internal
*/
abstract class AbstractNode implements NodeInterface
{
/**
* @var string
*/
private $nodeName;
public function getNodeName(): string
{
if (null === $this->nodeName) {
$this->nodeName = preg_replace('~.*\\\\([^\\\\]+)Node$~', '$1', static::class);
}
return $this->nodeName;
}
}

View File

@ -0,0 +1,82 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\Node;
/**
* Represents a "<selector>[<namespace>|<attribute> <operator> <value>]" node.
*
* This component is a port of the Python cssselect library,
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
*
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
*
* @internal
*/
class AttributeNode extends AbstractNode
{
private $selector;
private $namespace;
private $attribute;
private $operator;
private $value;
public function __construct(NodeInterface $selector, ?string $namespace, string $attribute, string $operator, ?string $value)
{
$this->selector = $selector;
$this->namespace = $namespace;
$this->attribute = $attribute;
$this->operator = $operator;
$this->value = $value;
}
public function getSelector(): NodeInterface
{
return $this->selector;
}
public function getNamespace(): ?string
{
return $this->namespace;
}
public function getAttribute(): string
{
return $this->attribute;
}
public function getOperator(): string
{
return $this->operator;
}
public function getValue(): ?string
{
return $this->value;
}
/**
* {@inheritdoc}
*/
public function getSpecificity(): Specificity
{
return $this->selector->getSpecificity()->plus(new Specificity(0, 1, 0));
}
public function __toString(): string
{
$attribute = $this->namespace ? $this->namespace.'|'.$this->attribute : $this->attribute;
return 'exists' === $this->operator
? sprintf('%s[%s[%s]]', $this->getNodeName(), $this->selector, $attribute)
: sprintf("%s[%s[%s %s '%s']]", $this->getNodeName(), $this->selector, $attribute, $this->operator, $this->value);
}
}

View File

@ -0,0 +1,57 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\Node;
/**
* Represents a "<selector>.<name>" node.
*
* This component is a port of the Python cssselect library,
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
*
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
*
* @internal
*/
class ClassNode extends AbstractNode
{
private $selector;
private $name;
public function __construct(NodeInterface $selector, string $name)
{
$this->selector = $selector;
$this->name = $name;
}
public function getSelector(): NodeInterface
{
return $this->selector;
}
public function getName(): string
{
return $this->name;
}
/**
* {@inheritdoc}
*/
public function getSpecificity(): Specificity
{
return $this->selector->getSpecificity()->plus(new Specificity(0, 1, 0));
}
public function __toString(): string
{
return sprintf('%s[%s.%s]', $this->getNodeName(), $this->selector, $this->name);
}
}

View File

@ -0,0 +1,66 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\Node;
/**
* Represents a combined node.
*
* This component is a port of the Python cssselect library,
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
*
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
*
* @internal
*/
class CombinedSelectorNode extends AbstractNode
{
private $selector;
private $combinator;
private $subSelector;
public function __construct(NodeInterface $selector, string $combinator, NodeInterface $subSelector)
{
$this->selector = $selector;
$this->combinator = $combinator;
$this->subSelector = $subSelector;
}
public function getSelector(): NodeInterface
{
return $this->selector;
}
public function getCombinator(): string
{
return $this->combinator;
}
public function getSubSelector(): NodeInterface
{
return $this->subSelector;
}
/**
* {@inheritdoc}
*/
public function getSpecificity(): Specificity
{
return $this->selector->getSpecificity()->plus($this->subSelector->getSpecificity());
}
public function __toString(): string
{
$combinator = ' ' === $this->combinator ? '<followed>' : $this->combinator;
return sprintf('%s[%s %s %s]', $this->getNodeName(), $this->selector, $combinator, $this->subSelector);
}
}

View File

@ -0,0 +1,59 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\Node;
/**
* Represents a "<namespace>|<element>" node.
*
* This component is a port of the Python cssselect library,
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
*
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
*
* @internal
*/
class ElementNode extends AbstractNode
{
private $namespace;
private $element;
public function __construct(string $namespace = null, string $element = null)
{
$this->namespace = $namespace;
$this->element = $element;
}
public function getNamespace(): ?string
{
return $this->namespace;
}
public function getElement(): ?string
{
return $this->element;
}
/**
* {@inheritdoc}
*/
public function getSpecificity(): Specificity
{
return new Specificity(0, 0, $this->element ? 1 : 0);
}
public function __toString(): string
{
$element = $this->element ?: '*';
return sprintf('%s[%s]', $this->getNodeName(), $this->namespace ? $this->namespace.'|'.$element : $element);
}
}

View File

@ -0,0 +1,76 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\Node;
use Symfony\Component\CssSelector\Parser\Token;
/**
* Represents a "<selector>:<name>(<arguments>)" node.
*
* This component is a port of the Python cssselect library,
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
*
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
*
* @internal
*/
class FunctionNode extends AbstractNode
{
private $selector;
private $name;
private $arguments;
/**
* @param Token[] $arguments
*/
public function __construct(NodeInterface $selector, string $name, array $arguments = [])
{
$this->selector = $selector;
$this->name = strtolower($name);
$this->arguments = $arguments;
}
public function getSelector(): NodeInterface
{
return $this->selector;
}
public function getName(): string
{
return $this->name;
}
/**
* @return Token[]
*/
public function getArguments(): array
{
return $this->arguments;
}
/**
* {@inheritdoc}
*/
public function getSpecificity(): Specificity
{
return $this->selector->getSpecificity()->plus(new Specificity(0, 1, 0));
}
public function __toString(): string
{
$arguments = implode(', ', array_map(function (Token $token) {
return "'".$token->getValue()."'";
}, $this->arguments));
return sprintf('%s[%s:%s(%s)]', $this->getNodeName(), $this->selector, $this->name, $arguments ? '['.$arguments.']' : '');
}
}

View File

@ -0,0 +1,57 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\Node;
/**
* Represents a "<selector>#<id>" node.
*
* This component is a port of the Python cssselect library,
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
*
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
*
* @internal
*/
class HashNode extends AbstractNode
{
private $selector;
private $id;
public function __construct(NodeInterface $selector, string $id)
{
$this->selector = $selector;
$this->id = $id;
}
public function getSelector(): NodeInterface
{
return $this->selector;
}
public function getId(): string
{
return $this->id;
}
/**
* {@inheritdoc}
*/
public function getSpecificity(): Specificity
{
return $this->selector->getSpecificity()->plus(new Specificity(1, 0, 0));
}
public function __toString(): string
{
return sprintf('%s[%s#%s]', $this->getNodeName(), $this->selector, $this->id);
}
}

View File

@ -0,0 +1,57 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\Node;
/**
* Represents a "<selector>:not(<identifier>)" node.
*
* This component is a port of the Python cssselect library,
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
*
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
*
* @internal
*/
class NegationNode extends AbstractNode
{
private $selector;
private $subSelector;
public function __construct(NodeInterface $selector, NodeInterface $subSelector)
{
$this->selector = $selector;
$this->subSelector = $subSelector;
}
public function getSelector(): NodeInterface
{
return $this->selector;
}
public function getSubSelector(): NodeInterface
{
return $this->subSelector;
}
/**
* {@inheritdoc}
*/
public function getSpecificity(): Specificity
{
return $this->selector->getSpecificity()->plus($this->subSelector->getSpecificity());
}
public function __toString(): string
{
return sprintf('%s[%s:not(%s)]', $this->getNodeName(), $this->selector, $this->subSelector);
}
}

View File

@ -0,0 +1,31 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\Node;
/**
* Interface for nodes.
*
* This component is a port of the Python cssselect library,
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
*
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
*
* @internal
*/
interface NodeInterface
{
public function getNodeName(): string;
public function getSpecificity(): Specificity;
public function __toString(): string;
}

View File

@ -0,0 +1,57 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\Node;
/**
* Represents a "<selector>:<identifier>" node.
*
* This component is a port of the Python cssselect library,
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
*
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
*
* @internal
*/
class PseudoNode extends AbstractNode
{
private $selector;
private $identifier;
public function __construct(NodeInterface $selector, string $identifier)
{
$this->selector = $selector;
$this->identifier = strtolower($identifier);
}
public function getSelector(): NodeInterface
{
return $this->selector;
}
public function getIdentifier(): string
{
return $this->identifier;
}
/**
* {@inheritdoc}
*/
public function getSpecificity(): Specificity
{
return $this->selector->getSpecificity()->plus(new Specificity(0, 1, 0));
}
public function __toString(): string
{
return sprintf('%s[%s:%s]', $this->getNodeName(), $this->selector, $this->identifier);
}
}

View File

@ -0,0 +1,57 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\Node;
/**
* Represents a "<selector>(::|:)<pseudoElement>" node.
*
* This component is a port of the Python cssselect library,
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
*
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
*
* @internal
*/
class SelectorNode extends AbstractNode
{
private $tree;
private $pseudoElement;
public function __construct(NodeInterface $tree, string $pseudoElement = null)
{
$this->tree = $tree;
$this->pseudoElement = $pseudoElement ? strtolower($pseudoElement) : null;
}
public function getTree(): NodeInterface
{
return $this->tree;
}
public function getPseudoElement(): ?string
{
return $this->pseudoElement;
}
/**
* {@inheritdoc}
*/
public function getSpecificity(): Specificity
{
return $this->tree->getSpecificity()->plus(new Specificity(0, 0, $this->pseudoElement ? 1 : 0));
}
public function __toString(): string
{
return sprintf('%s[%s%s]', $this->getNodeName(), $this->tree, $this->pseudoElement ? '::'.$this->pseudoElement : '');
}
}

View File

@ -0,0 +1,73 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\Node;
/**
* Represents a node specificity.
*
* This component is a port of the Python cssselect library,
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
*
* @see http://www.w3.org/TR/selectors/#specificity
*
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
*
* @internal
*/
class Specificity
{
public const A_FACTOR = 100;
public const B_FACTOR = 10;
public const C_FACTOR = 1;
private $a;
private $b;
private $c;
public function __construct(int $a, int $b, int $c)
{
$this->a = $a;
$this->b = $b;
$this->c = $c;
}
public function plus(self $specificity): self
{
return new self($this->a + $specificity->a, $this->b + $specificity->b, $this->c + $specificity->c);
}
public function getValue(): int
{
return $this->a * self::A_FACTOR + $this->b * self::B_FACTOR + $this->c * self::C_FACTOR;
}
/**
* Returns -1 if the object specificity is lower than the argument,
* 0 if they are equal, and 1 if the argument is lower.
*/
public function compareTo(self $specificity): int
{
if ($this->a !== $specificity->a) {
return $this->a > $specificity->a ? 1 : -1;
}
if ($this->b !== $specificity->b) {
return $this->b > $specificity->b ? 1 : -1;
}
if ($this->c !== $specificity->c) {
return $this->c > $specificity->c ? 1 : -1;
}
return 0;
}
}

View File

@ -0,0 +1,48 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\Parser\Handler;
use Symfony\Component\CssSelector\Parser\Reader;
use Symfony\Component\CssSelector\Parser\TokenStream;
/**
* CSS selector comment handler.
*
* This component is a port of the Python cssselect library,
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
*
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
*
* @internal
*/
class CommentHandler implements HandlerInterface
{
/**
* {@inheritdoc}
*/
public function handle(Reader $reader, TokenStream $stream): bool
{
if ('/*' !== $reader->getSubstring(2)) {
return false;
}
$offset = $reader->getOffset('*/');
if (false === $offset) {
$reader->moveToEnd();
} else {
$reader->moveForward($offset + 2);
}
return true;
}
}

View File

@ -0,0 +1,30 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\Parser\Handler;
use Symfony\Component\CssSelector\Parser\Reader;
use Symfony\Component\CssSelector\Parser\TokenStream;
/**
* CSS selector handler interface.
*
* This component is a port of the Python cssselect library,
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
*
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
*
* @internal
*/
interface HandlerInterface
{
public function handle(Reader $reader, TokenStream $stream): bool;
}

View File

@ -0,0 +1,58 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\Parser\Handler;
use Symfony\Component\CssSelector\Parser\Reader;
use Symfony\Component\CssSelector\Parser\Token;
use Symfony\Component\CssSelector\Parser\Tokenizer\TokenizerEscaping;
use Symfony\Component\CssSelector\Parser\Tokenizer\TokenizerPatterns;
use Symfony\Component\CssSelector\Parser\TokenStream;
/**
* CSS selector comment handler.
*
* This component is a port of the Python cssselect library,
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
*
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
*
* @internal
*/
class HashHandler implements HandlerInterface
{
private $patterns;
private $escaping;
public function __construct(TokenizerPatterns $patterns, TokenizerEscaping $escaping)
{
$this->patterns = $patterns;
$this->escaping = $escaping;
}
/**
* {@inheritdoc}
*/
public function handle(Reader $reader, TokenStream $stream): bool
{
$match = $reader->findPattern($this->patterns->getHashPattern());
if (!$match) {
return false;
}
$value = $this->escaping->escapeUnicode($match[1]);
$stream->push(new Token(Token::TYPE_HASH, $value, $reader->getPosition()));
$reader->moveForward(\strlen($match[0]));
return true;
}
}

View File

@ -0,0 +1,58 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\Parser\Handler;
use Symfony\Component\CssSelector\Parser\Reader;
use Symfony\Component\CssSelector\Parser\Token;
use Symfony\Component\CssSelector\Parser\Tokenizer\TokenizerEscaping;
use Symfony\Component\CssSelector\Parser\Tokenizer\TokenizerPatterns;
use Symfony\Component\CssSelector\Parser\TokenStream;
/**
* CSS selector comment handler.
*
* This component is a port of the Python cssselect library,
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
*
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
*
* @internal
*/
class IdentifierHandler implements HandlerInterface
{
private $patterns;
private $escaping;
public function __construct(TokenizerPatterns $patterns, TokenizerEscaping $escaping)
{
$this->patterns = $patterns;
$this->escaping = $escaping;
}
/**
* {@inheritdoc}
*/
public function handle(Reader $reader, TokenStream $stream): bool
{
$match = $reader->findPattern($this->patterns->getIdentifierPattern());
if (!$match) {
return false;
}
$value = $this->escaping->escapeUnicode($match[0]);
$stream->push(new Token(Token::TYPE_IDENTIFIER, $value, $reader->getPosition()));
$reader->moveForward(\strlen($match[0]));
return true;
}
}

View File

@ -0,0 +1,54 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\Parser\Handler;
use Symfony\Component\CssSelector\Parser\Reader;
use Symfony\Component\CssSelector\Parser\Token;
use Symfony\Component\CssSelector\Parser\Tokenizer\TokenizerPatterns;
use Symfony\Component\CssSelector\Parser\TokenStream;
/**
* CSS selector comment handler.
*
* This component is a port of the Python cssselect library,
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
*
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
*
* @internal
*/
class NumberHandler implements HandlerInterface
{
private $patterns;
public function __construct(TokenizerPatterns $patterns)
{
$this->patterns = $patterns;
}
/**
* {@inheritdoc}
*/
public function handle(Reader $reader, TokenStream $stream): bool
{
$match = $reader->findPattern($this->patterns->getNumberPattern());
if (!$match) {
return false;
}
$stream->push(new Token(Token::TYPE_NUMBER, $match[0], $reader->getPosition()));
$reader->moveForward(\strlen($match[0]));
return true;
}
}

View File

@ -0,0 +1,77 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\Parser\Handler;
use Symfony\Component\CssSelector\Exception\InternalErrorException;
use Symfony\Component\CssSelector\Exception\SyntaxErrorException;
use Symfony\Component\CssSelector\Parser\Reader;
use Symfony\Component\CssSelector\Parser\Token;
use Symfony\Component\CssSelector\Parser\Tokenizer\TokenizerEscaping;
use Symfony\Component\CssSelector\Parser\Tokenizer\TokenizerPatterns;
use Symfony\Component\CssSelector\Parser\TokenStream;
/**
* CSS selector comment handler.
*
* This component is a port of the Python cssselect library,
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
*
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
*
* @internal
*/
class StringHandler implements HandlerInterface
{
private $patterns;
private $escaping;
public function __construct(TokenizerPatterns $patterns, TokenizerEscaping $escaping)
{
$this->patterns = $patterns;
$this->escaping = $escaping;
}
/**
* {@inheritdoc}
*/
public function handle(Reader $reader, TokenStream $stream): bool
{
$quote = $reader->getSubstring(1);
if (!\in_array($quote, ["'", '"'])) {
return false;
}
$reader->moveForward(1);
$match = $reader->findPattern($this->patterns->getQuotedStringPattern($quote));
if (!$match) {
throw new InternalErrorException(sprintf('Should have found at least an empty match at %d.', $reader->getPosition()));
}
// check unclosed strings
if (\strlen($match[0]) === $reader->getRemainingLength()) {
throw SyntaxErrorException::unclosedString($reader->getPosition() - 1);
}
// check quotes pairs validity
if ($quote !== $reader->getSubstring(1, \strlen($match[0]))) {
throw SyntaxErrorException::unclosedString($reader->getPosition() - 1);
}
$string = $this->escaping->escapeUnicodeAndNewLine($match[0]);
$stream->push(new Token(Token::TYPE_STRING, $string, $reader->getPosition()));
$reader->moveForward(\strlen($match[0]) + 1);
return true;
}
}

View File

@ -0,0 +1,46 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\Parser\Handler;
use Symfony\Component\CssSelector\Parser\Reader;
use Symfony\Component\CssSelector\Parser\Token;
use Symfony\Component\CssSelector\Parser\TokenStream;
/**
* CSS selector whitespace handler.
*
* This component is a port of the Python cssselect library,
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
*
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
*
* @internal
*/
class WhitespaceHandler implements HandlerInterface
{
/**
* {@inheritdoc}
*/
public function handle(Reader $reader, TokenStream $stream): bool
{
$match = $reader->findPattern('~^[ \t\r\n\f]+~');
if (false === $match) {
return false;
}
$stream->push(new Token(Token::TYPE_WHITESPACE, $match[0], $reader->getPosition()));
$reader->moveForward(\strlen($match[0]));
return true;
}
}

View File

@ -0,0 +1,353 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\Parser;
use Symfony\Component\CssSelector\Exception\SyntaxErrorException;
use Symfony\Component\CssSelector\Node;
use Symfony\Component\CssSelector\Parser\Tokenizer\Tokenizer;
/**
* CSS selector parser.
*
* This component is a port of the Python cssselect library,
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
*
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
*
* @internal
*/
class Parser implements ParserInterface
{
private $tokenizer;
public function __construct(Tokenizer $tokenizer = null)
{
$this->tokenizer = $tokenizer ?? new Tokenizer();
}
/**
* {@inheritdoc}
*/
public function parse(string $source): array
{
$reader = new Reader($source);
$stream = $this->tokenizer->tokenize($reader);
return $this->parseSelectorList($stream);
}
/**
* Parses the arguments for ":nth-child()" and friends.
*
* @param Token[] $tokens
*
* @throws SyntaxErrorException
*/
public static function parseSeries(array $tokens): array
{
foreach ($tokens as $token) {
if ($token->isString()) {
throw SyntaxErrorException::stringAsFunctionArgument();
}
}
$joined = trim(implode('', array_map(function (Token $token) {
return $token->getValue();
}, $tokens)));
$int = function ($string) {
if (!is_numeric($string)) {
throw SyntaxErrorException::stringAsFunctionArgument();
}
return (int) $string;
};
switch (true) {
case 'odd' === $joined:
return [2, 1];
case 'even' === $joined:
return [2, 0];
case 'n' === $joined:
return [1, 0];
case !str_contains($joined, 'n'):
return [0, $int($joined)];
}
$split = explode('n', $joined);
$first = $split[0] ?? null;
return [
$first ? ('-' === $first || '+' === $first ? $int($first.'1') : $int($first)) : 1,
isset($split[1]) && $split[1] ? $int($split[1]) : 0,
];
}
private function parseSelectorList(TokenStream $stream): array
{
$stream->skipWhitespace();
$selectors = [];
while (true) {
$selectors[] = $this->parserSelectorNode($stream);
if ($stream->getPeek()->isDelimiter([','])) {
$stream->getNext();
$stream->skipWhitespace();
} else {
break;
}
}
return $selectors;
}
private function parserSelectorNode(TokenStream $stream): Node\SelectorNode
{
[$result, $pseudoElement] = $this->parseSimpleSelector($stream);
while (true) {
$stream->skipWhitespace();
$peek = $stream->getPeek();
if ($peek->isFileEnd() || $peek->isDelimiter([','])) {
break;
}
if (null !== $pseudoElement) {
throw SyntaxErrorException::pseudoElementFound($pseudoElement, 'not at the end of a selector');
}
if ($peek->isDelimiter(['+', '>', '~'])) {
$combinator = $stream->getNext()->getValue();
$stream->skipWhitespace();
} else {
$combinator = ' ';
}
[$nextSelector, $pseudoElement] = $this->parseSimpleSelector($stream);
$result = new Node\CombinedSelectorNode($result, $combinator, $nextSelector);
}
return new Node\SelectorNode($result, $pseudoElement);
}
/**
* Parses next simple node (hash, class, pseudo, negation).
*
* @throws SyntaxErrorException
*/
private function parseSimpleSelector(TokenStream $stream, bool $insideNegation = false): array
{
$stream->skipWhitespace();
$selectorStart = \count($stream->getUsed());
$result = $this->parseElementNode($stream);
$pseudoElement = null;
while (true) {
$peek = $stream->getPeek();
if ($peek->isWhitespace()
|| $peek->isFileEnd()
|| $peek->isDelimiter([',', '+', '>', '~'])
|| ($insideNegation && $peek->isDelimiter([')']))
) {
break;
}
if (null !== $pseudoElement) {
throw SyntaxErrorException::pseudoElementFound($pseudoElement, 'not at the end of a selector');
}
if ($peek->isHash()) {
$result = new Node\HashNode($result, $stream->getNext()->getValue());
} elseif ($peek->isDelimiter(['.'])) {
$stream->getNext();
$result = new Node\ClassNode($result, $stream->getNextIdentifier());
} elseif ($peek->isDelimiter(['['])) {
$stream->getNext();
$result = $this->parseAttributeNode($result, $stream);
} elseif ($peek->isDelimiter([':'])) {
$stream->getNext();
if ($stream->getPeek()->isDelimiter([':'])) {
$stream->getNext();
$pseudoElement = $stream->getNextIdentifier();
continue;
}
$identifier = $stream->getNextIdentifier();
if (\in_array(strtolower($identifier), ['first-line', 'first-letter', 'before', 'after'])) {
// Special case: CSS 2.1 pseudo-elements can have a single ':'.
// Any new pseudo-element must have two.
$pseudoElement = $identifier;
continue;
}
if (!$stream->getPeek()->isDelimiter(['('])) {
$result = new Node\PseudoNode($result, $identifier);
continue;
}
$stream->getNext();
$stream->skipWhitespace();
if ('not' === strtolower($identifier)) {
if ($insideNegation) {
throw SyntaxErrorException::nestedNot();
}
[$argument, $argumentPseudoElement] = $this->parseSimpleSelector($stream, true);
$next = $stream->getNext();
if (null !== $argumentPseudoElement) {
throw SyntaxErrorException::pseudoElementFound($argumentPseudoElement, 'inside ::not()');
}
if (!$next->isDelimiter([')'])) {
throw SyntaxErrorException::unexpectedToken('")"', $next);
}
$result = new Node\NegationNode($result, $argument);
} else {
$arguments = [];
$next = null;
while (true) {
$stream->skipWhitespace();
$next = $stream->getNext();
if ($next->isIdentifier()
|| $next->isString()
|| $next->isNumber()
|| $next->isDelimiter(['+', '-'])
) {
$arguments[] = $next;
} elseif ($next->isDelimiter([')'])) {
break;
} else {
throw SyntaxErrorException::unexpectedToken('an argument', $next);
}
}
if (empty($arguments)) {
throw SyntaxErrorException::unexpectedToken('at least one argument', $next);
}
$result = new Node\FunctionNode($result, $identifier, $arguments);
}
} else {
throw SyntaxErrorException::unexpectedToken('selector', $peek);
}
}
if (\count($stream->getUsed()) === $selectorStart) {
throw SyntaxErrorException::unexpectedToken('selector', $stream->getPeek());
}
return [$result, $pseudoElement];
}
private function parseElementNode(TokenStream $stream): Node\ElementNode
{
$peek = $stream->getPeek();
if ($peek->isIdentifier() || $peek->isDelimiter(['*'])) {
if ($peek->isIdentifier()) {
$namespace = $stream->getNext()->getValue();
} else {
$stream->getNext();
$namespace = null;
}
if ($stream->getPeek()->isDelimiter(['|'])) {
$stream->getNext();
$element = $stream->getNextIdentifierOrStar();
} else {
$element = $namespace;
$namespace = null;
}
} else {
$element = $namespace = null;
}
return new Node\ElementNode($namespace, $element);
}
private function parseAttributeNode(Node\NodeInterface $selector, TokenStream $stream): Node\AttributeNode
{
$stream->skipWhitespace();
$attribute = $stream->getNextIdentifierOrStar();
if (null === $attribute && !$stream->getPeek()->isDelimiter(['|'])) {
throw SyntaxErrorException::unexpectedToken('"|"', $stream->getPeek());
}
if ($stream->getPeek()->isDelimiter(['|'])) {
$stream->getNext();
if ($stream->getPeek()->isDelimiter(['='])) {
$namespace = null;
$stream->getNext();
$operator = '|=';
} else {
$namespace = $attribute;
$attribute = $stream->getNextIdentifier();
$operator = null;
}
} else {
$namespace = $operator = null;
}
if (null === $operator) {
$stream->skipWhitespace();
$next = $stream->getNext();
if ($next->isDelimiter([']'])) {
return new Node\AttributeNode($selector, $namespace, $attribute, 'exists', null);
} elseif ($next->isDelimiter(['='])) {
$operator = '=';
} elseif ($next->isDelimiter(['^', '$', '*', '~', '|', '!'])
&& $stream->getPeek()->isDelimiter(['='])
) {
$operator = $next->getValue().'=';
$stream->getNext();
} else {
throw SyntaxErrorException::unexpectedToken('operator', $next);
}
}
$stream->skipWhitespace();
$value = $stream->getNext();
if ($value->isNumber()) {
// if the value is a number, it's casted into a string
$value = new Token(Token::TYPE_STRING, (string) $value->getValue(), $value->getPosition());
}
if (!($value->isIdentifier() || $value->isString())) {
throw SyntaxErrorException::unexpectedToken('string or identifier', $value);
}
$stream->skipWhitespace();
$next = $stream->getNext();
if (!$next->isDelimiter([']'])) {
throw SyntaxErrorException::unexpectedToken('"]"', $next);
}
return new Node\AttributeNode($selector, $namespace, $attribute, $operator, $value->getValue());
}
}

View File

@ -0,0 +1,34 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\Parser;
use Symfony\Component\CssSelector\Node\SelectorNode;
/**
* CSS selector parser interface.
*
* This component is a port of the Python cssselect library,
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
*
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
*
* @internal
*/
interface ParserInterface
{
/**
* Parses given selector source into an array of tokens.
*
* @return SelectorNode[]
*/
public function parse(string $source): array;
}

View File

@ -0,0 +1,86 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\Parser;
/**
* CSS selector reader.
*
* This component is a port of the Python cssselect library,
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
*
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
*
* @internal
*/
class Reader
{
private $source;
private $length;
private $position = 0;
public function __construct(string $source)
{
$this->source = $source;
$this->length = \strlen($source);
}
public function isEOF(): bool
{
return $this->position >= $this->length;
}
public function getPosition(): int
{
return $this->position;
}
public function getRemainingLength(): int
{
return $this->length - $this->position;
}
public function getSubstring(int $length, int $offset = 0): string
{
return substr($this->source, $this->position + $offset, $length);
}
public function getOffset(string $string)
{
$position = strpos($this->source, $string, $this->position);
return false === $position ? false : $position - $this->position;
}
/**
* @return array|false
*/
public function findPattern(string $pattern)
{
$source = substr($this->source, $this->position);
if (preg_match($pattern, $source, $matches)) {
return $matches;
}
return false;
}
public function moveForward(int $length)
{
$this->position += $length;
}
public function moveToEnd()
{
$this->position = $this->length;
}
}

View File

@ -0,0 +1,51 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\Parser\Shortcut;
use Symfony\Component\CssSelector\Node\ClassNode;
use Symfony\Component\CssSelector\Node\ElementNode;
use Symfony\Component\CssSelector\Node\SelectorNode;
use Symfony\Component\CssSelector\Parser\ParserInterface;
/**
* CSS selector class parser shortcut.
*
* This component is a port of the Python cssselect library,
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
*
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
*
* @internal
*/
class ClassParser implements ParserInterface
{
/**
* {@inheritdoc}
*/
public function parse(string $source): array
{
// Matches an optional namespace, optional element, and required class
// $source = 'test|input.ab6bd_field';
// $matches = array (size=4)
// 0 => string 'test|input.ab6bd_field' (length=22)
// 1 => string 'test' (length=4)
// 2 => string 'input' (length=5)
// 3 => string 'ab6bd_field' (length=11)
if (preg_match('/^(?:([a-z]++)\|)?+([\w-]++|\*)?+\.([\w-]++)$/i', trim($source), $matches)) {
return [
new SelectorNode(new ClassNode(new ElementNode($matches[1] ?: null, $matches[2] ?: null), $matches[3])),
];
}
return [];
}
}

View File

@ -0,0 +1,47 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\Parser\Shortcut;
use Symfony\Component\CssSelector\Node\ElementNode;
use Symfony\Component\CssSelector\Node\SelectorNode;
use Symfony\Component\CssSelector\Parser\ParserInterface;
/**
* CSS selector element parser shortcut.
*
* This component is a port of the Python cssselect library,
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
*
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
*
* @internal
*/
class ElementParser implements ParserInterface
{
/**
* {@inheritdoc}
*/
public function parse(string $source): array
{
// Matches an optional namespace, required element or `*`
// $source = 'testns|testel';
// $matches = array (size=3)
// 0 => string 'testns|testel' (length=13)
// 1 => string 'testns' (length=6)
// 2 => string 'testel' (length=6)
if (preg_match('/^(?:([a-z]++)\|)?([\w-]++|\*)$/i', trim($source), $matches)) {
return [new SelectorNode(new ElementNode($matches[1] ?: null, $matches[2]))];
}
return [];
}
}

View File

@ -0,0 +1,46 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\Parser\Shortcut;
use Symfony\Component\CssSelector\Node\ElementNode;
use Symfony\Component\CssSelector\Node\SelectorNode;
use Symfony\Component\CssSelector\Parser\ParserInterface;
/**
* CSS selector class parser shortcut.
*
* This shortcut ensure compatibility with previous version.
* - The parser fails to parse an empty string.
* - In the previous version, an empty string matches each tags.
*
* This component is a port of the Python cssselect library,
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
*
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
*
* @internal
*/
class EmptyStringParser implements ParserInterface
{
/**
* {@inheritdoc}
*/
public function parse(string $source): array
{
// Matches an empty string
if ('' == $source) {
return [new SelectorNode(new ElementNode(null, '*'))];
}
return [];
}
}

View File

@ -0,0 +1,51 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\Parser\Shortcut;
use Symfony\Component\CssSelector\Node\ElementNode;
use Symfony\Component\CssSelector\Node\HashNode;
use Symfony\Component\CssSelector\Node\SelectorNode;
use Symfony\Component\CssSelector\Parser\ParserInterface;
/**
* CSS selector hash parser shortcut.
*
* This component is a port of the Python cssselect library,
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
*
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
*
* @internal
*/
class HashParser implements ParserInterface
{
/**
* {@inheritdoc}
*/
public function parse(string $source): array
{
// Matches an optional namespace, optional element, and required id
// $source = 'test|input#ab6bd_field';
// $matches = array (size=4)
// 0 => string 'test|input#ab6bd_field' (length=22)
// 1 => string 'test' (length=4)
// 2 => string 'input' (length=5)
// 3 => string 'ab6bd_field' (length=11)
if (preg_match('/^(?:([a-z]++)\|)?+([\w-]++|\*)?+#([\w-]++)$/i', trim($source), $matches)) {
return [
new SelectorNode(new HashNode(new ElementNode($matches[1] ?: null, $matches[2] ?: null), $matches[3])),
];
}
return [];
}
}

View File

@ -0,0 +1,111 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\Parser;
/**
* CSS selector token.
*
* This component is a port of the Python cssselect library,
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
*
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
*
* @internal
*/
class Token
{
public const TYPE_FILE_END = 'eof';
public const TYPE_DELIMITER = 'delimiter';
public const TYPE_WHITESPACE = 'whitespace';
public const TYPE_IDENTIFIER = 'identifier';
public const TYPE_HASH = 'hash';
public const TYPE_NUMBER = 'number';
public const TYPE_STRING = 'string';
private $type;
private $value;
private $position;
public function __construct(?string $type, ?string $value, ?int $position)
{
$this->type = $type;
$this->value = $value;
$this->position = $position;
}
public function getType(): ?int
{
return $this->type;
}
public function getValue(): ?string
{
return $this->value;
}
public function getPosition(): ?int
{
return $this->position;
}
public function isFileEnd(): bool
{
return self::TYPE_FILE_END === $this->type;
}
public function isDelimiter(array $values = []): bool
{
if (self::TYPE_DELIMITER !== $this->type) {
return false;
}
if (empty($values)) {
return true;
}
return \in_array($this->value, $values);
}
public function isWhitespace(): bool
{
return self::TYPE_WHITESPACE === $this->type;
}
public function isIdentifier(): bool
{
return self::TYPE_IDENTIFIER === $this->type;
}
public function isHash(): bool
{
return self::TYPE_HASH === $this->type;
}
public function isNumber(): bool
{
return self::TYPE_NUMBER === $this->type;
}
public function isString(): bool
{
return self::TYPE_STRING === $this->type;
}
public function __toString(): string
{
if ($this->value) {
return sprintf('<%s "%s" at %s>', $this->type, $this->value, $this->position);
}
return sprintf('<%s at %s>', $this->type, $this->position);
}
}

View File

@ -0,0 +1,171 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\Parser;
use Symfony\Component\CssSelector\Exception\InternalErrorException;
use Symfony\Component\CssSelector\Exception\SyntaxErrorException;
/**
* CSS selector token stream.
*
* This component is a port of the Python cssselect library,
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
*
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
*
* @internal
*/
class TokenStream
{
/**
* @var Token[]
*/
private $tokens = [];
/**
* @var Token[]
*/
private $used = [];
/**
* @var int
*/
private $cursor = 0;
/**
* @var Token|null
*/
private $peeked;
/**
* @var bool
*/
private $peeking = false;
/**
* Pushes a token.
*
* @return $this
*/
public function push(Token $token): self
{
$this->tokens[] = $token;
return $this;
}
/**
* Freezes stream.
*
* @return $this
*/
public function freeze(): self
{
return $this;
}
/**
* Returns next token.
*
* @throws InternalErrorException If there is no more token
*/
public function getNext(): Token
{
if ($this->peeking) {
$this->peeking = false;
$this->used[] = $this->peeked;
return $this->peeked;
}
if (!isset($this->tokens[$this->cursor])) {
throw new InternalErrorException('Unexpected token stream end.');
}
return $this->tokens[$this->cursor++];
}
/**
* Returns peeked token.
*/
public function getPeek(): Token
{
if (!$this->peeking) {
$this->peeked = $this->getNext();
$this->peeking = true;
}
return $this->peeked;
}
/**
* Returns used tokens.
*
* @return Token[]
*/
public function getUsed(): array
{
return $this->used;
}
/**
* Returns nex identifier token.
*
* @return string The identifier token value
*
* @throws SyntaxErrorException If next token is not an identifier
*/
public function getNextIdentifier(): string
{
$next = $this->getNext();
if (!$next->isIdentifier()) {
throw SyntaxErrorException::unexpectedToken('identifier', $next);
}
return $next->getValue();
}
/**
* Returns nex identifier or star delimiter token.
*
* @return string|null The identifier token value or null if star found
*
* @throws SyntaxErrorException If next token is not an identifier or a star delimiter
*/
public function getNextIdentifierOrStar(): ?string
{
$next = $this->getNext();
if ($next->isIdentifier()) {
return $next->getValue();
}
if ($next->isDelimiter(['*'])) {
return null;
}
throw SyntaxErrorException::unexpectedToken('identifier or "*"', $next);
}
/**
* Skips next whitespace if any.
*/
public function skipWhitespace()
{
$peek = $this->getPeek();
if ($peek->isWhitespace()) {
$this->getNext();
}
}
}

View File

@ -0,0 +1,73 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\Parser\Tokenizer;
use Symfony\Component\CssSelector\Parser\Handler;
use Symfony\Component\CssSelector\Parser\Reader;
use Symfony\Component\CssSelector\Parser\Token;
use Symfony\Component\CssSelector\Parser\TokenStream;
/**
* CSS selector tokenizer.
*
* This component is a port of the Python cssselect library,
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
*
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
*
* @internal
*/
class Tokenizer
{
/**
* @var Handler\HandlerInterface[]
*/
private $handlers;
public function __construct()
{
$patterns = new TokenizerPatterns();
$escaping = new TokenizerEscaping($patterns);
$this->handlers = [
new Handler\WhitespaceHandler(),
new Handler\IdentifierHandler($patterns, $escaping),
new Handler\HashHandler($patterns, $escaping),
new Handler\StringHandler($patterns, $escaping),
new Handler\NumberHandler($patterns),
new Handler\CommentHandler(),
];
}
/**
* Tokenize selector source code.
*/
public function tokenize(Reader $reader): TokenStream
{
$stream = new TokenStream();
while (!$reader->isEOF()) {
foreach ($this->handlers as $handler) {
if ($handler->handle($reader, $stream)) {
continue 2;
}
}
$stream->push(new Token(Token::TYPE_DELIMITER, $reader->getSubstring(1), $reader->getPosition()));
$reader->moveForward(1);
}
return $stream
->push(new Token(Token::TYPE_FILE_END, null, $reader->getPosition()))
->freeze();
}
}

View File

@ -0,0 +1,65 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\Parser\Tokenizer;
/**
* CSS selector tokenizer escaping applier.
*
* This component is a port of the Python cssselect library,
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
*
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
*
* @internal
*/
class TokenizerEscaping
{
private $patterns;
public function __construct(TokenizerPatterns $patterns)
{
$this->patterns = $patterns;
}
public function escapeUnicode(string $value): string
{
$value = $this->replaceUnicodeSequences($value);
return preg_replace($this->patterns->getSimpleEscapePattern(), '$1', $value);
}
public function escapeUnicodeAndNewLine(string $value): string
{
$value = preg_replace($this->patterns->getNewLineEscapePattern(), '', $value);
return $this->escapeUnicode($value);
}
private function replaceUnicodeSequences(string $value): string
{
return preg_replace_callback($this->patterns->getUnicodeEscapePattern(), function ($match) {
$c = hexdec($match[1]);
if (0x80 > $c %= 0x200000) {
return \chr($c);
}
if (0x800 > $c) {
return \chr(0xC0 | $c >> 6).\chr(0x80 | $c & 0x3F);
}
if (0x10000 > $c) {
return \chr(0xE0 | $c >> 12).\chr(0x80 | $c >> 6 & 0x3F).\chr(0x80 | $c & 0x3F);
}
return '';
}, $value);
}
}

View File

@ -0,0 +1,89 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\Parser\Tokenizer;
/**
* CSS selector tokenizer patterns builder.
*
* This component is a port of the Python cssselect library,
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
*
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
*
* @internal
*/
class TokenizerPatterns
{
private $unicodeEscapePattern;
private $simpleEscapePattern;
private $newLineEscapePattern;
private $escapePattern;
private $stringEscapePattern;
private $nonAsciiPattern;
private $nmCharPattern;
private $nmStartPattern;
private $identifierPattern;
private $hashPattern;
private $numberPattern;
private $quotedStringPattern;
public function __construct()
{
$this->unicodeEscapePattern = '\\\\([0-9a-f]{1,6})(?:\r\n|[ \n\r\t\f])?';
$this->simpleEscapePattern = '\\\\(.)';
$this->newLineEscapePattern = '\\\\(?:\n|\r\n|\r|\f)';
$this->escapePattern = $this->unicodeEscapePattern.'|\\\\[^\n\r\f0-9a-f]';
$this->stringEscapePattern = $this->newLineEscapePattern.'|'.$this->escapePattern;
$this->nonAsciiPattern = '[^\x00-\x7F]';
$this->nmCharPattern = '[_a-z0-9-]|'.$this->escapePattern.'|'.$this->nonAsciiPattern;
$this->nmStartPattern = '[_a-z]|'.$this->escapePattern.'|'.$this->nonAsciiPattern;
$this->identifierPattern = '-?(?:'.$this->nmStartPattern.')(?:'.$this->nmCharPattern.')*';
$this->hashPattern = '#((?:'.$this->nmCharPattern.')+)';
$this->numberPattern = '[+-]?(?:[0-9]*\.[0-9]+|[0-9]+)';
$this->quotedStringPattern = '([^\n\r\f%s]|'.$this->stringEscapePattern.')*';
}
public function getNewLineEscapePattern(): string
{
return '~^'.$this->newLineEscapePattern.'~';
}
public function getSimpleEscapePattern(): string
{
return '~^'.$this->simpleEscapePattern.'~';
}
public function getUnicodeEscapePattern(): string
{
return '~^'.$this->unicodeEscapePattern.'~i';
}
public function getIdentifierPattern(): string
{
return '~^'.$this->identifierPattern.'~i';
}
public function getHashPattern(): string
{
return '~^'.$this->hashPattern.'~i';
}
public function getNumberPattern(): string
{
return '~^'.$this->numberPattern.'~';
}
public function getQuotedStringPattern(string $quote): string
{
return '~^'.sprintf($this->quotedStringPattern, $quote).'~i';
}
}

View File

@ -0,0 +1,20 @@
CssSelector Component
=====================
The CssSelector component converts CSS selectors to XPath expressions.
Resources
---------
* [Documentation](https://symfony.com/doc/current/components/css_selector.html)
* [Contributing](https://symfony.com/doc/current/contributing/index.html)
* [Report issues](https://github.com/symfony/symfony/issues) and
[send Pull Requests](https://github.com/symfony/symfony/pulls)
in the [main Symfony repository](https://github.com/symfony/symfony)
Credits
-------
This component is a port of the Python cssselect library
[v0.7.1](https://github.com/SimonSapin/cssselect/releases/tag/v0.7.1),
which is distributed under the BSD license.

View File

@ -0,0 +1,65 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\XPath\Extension;
/**
* XPath expression translator abstract extension.
*
* This component is a port of the Python cssselect library,
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
*
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
*
* @internal
*/
abstract class AbstractExtension implements ExtensionInterface
{
/**
* {@inheritdoc}
*/
public function getNodeTranslators(): array
{
return [];
}
/**
* {@inheritdoc}
*/
public function getCombinationTranslators(): array
{
return [];
}
/**
* {@inheritdoc}
*/
public function getFunctionTranslators(): array
{
return [];
}
/**
* {@inheritdoc}
*/
public function getPseudoClassTranslators(): array
{
return [];
}
/**
* {@inheritdoc}
*/
public function getAttributeMatchingTranslators(): array
{
return [];
}
}

View File

@ -0,0 +1,119 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\XPath\Extension;
use Symfony\Component\CssSelector\XPath\Translator;
use Symfony\Component\CssSelector\XPath\XPathExpr;
/**
* XPath expression translator attribute extension.
*
* This component is a port of the Python cssselect library,
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
*
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
*
* @internal
*/
class AttributeMatchingExtension extends AbstractExtension
{
/**
* {@inheritdoc}
*/
public function getAttributeMatchingTranslators(): array
{
return [
'exists' => [$this, 'translateExists'],
'=' => [$this, 'translateEquals'],
'~=' => [$this, 'translateIncludes'],
'|=' => [$this, 'translateDashMatch'],
'^=' => [$this, 'translatePrefixMatch'],
'$=' => [$this, 'translateSuffixMatch'],
'*=' => [$this, 'translateSubstringMatch'],
'!=' => [$this, 'translateDifferent'],
];
}
public function translateExists(XPathExpr $xpath, string $attribute, ?string $value): XPathExpr
{
return $xpath->addCondition($attribute);
}
public function translateEquals(XPathExpr $xpath, string $attribute, ?string $value): XPathExpr
{
return $xpath->addCondition(sprintf('%s = %s', $attribute, Translator::getXpathLiteral($value)));
}
public function translateIncludes(XPathExpr $xpath, string $attribute, ?string $value): XPathExpr
{
return $xpath->addCondition($value ? sprintf(
'%1$s and contains(concat(\' \', normalize-space(%1$s), \' \'), %2$s)',
$attribute,
Translator::getXpathLiteral(' '.$value.' ')
) : '0');
}
public function translateDashMatch(XPathExpr $xpath, string $attribute, ?string $value): XPathExpr
{
return $xpath->addCondition(sprintf(
'%1$s and (%1$s = %2$s or starts-with(%1$s, %3$s))',
$attribute,
Translator::getXpathLiteral($value),
Translator::getXpathLiteral($value.'-')
));
}
public function translatePrefixMatch(XPathExpr $xpath, string $attribute, ?string $value): XPathExpr
{
return $xpath->addCondition($value ? sprintf(
'%1$s and starts-with(%1$s, %2$s)',
$attribute,
Translator::getXpathLiteral($value)
) : '0');
}
public function translateSuffixMatch(XPathExpr $xpath, string $attribute, ?string $value): XPathExpr
{
return $xpath->addCondition($value ? sprintf(
'%1$s and substring(%1$s, string-length(%1$s)-%2$s) = %3$s',
$attribute,
\strlen($value) - 1,
Translator::getXpathLiteral($value)
) : '0');
}
public function translateSubstringMatch(XPathExpr $xpath, string $attribute, ?string $value): XPathExpr
{
return $xpath->addCondition($value ? sprintf(
'%1$s and contains(%1$s, %2$s)',
$attribute,
Translator::getXpathLiteral($value)
) : '0');
}
public function translateDifferent(XPathExpr $xpath, string $attribute, ?string $value): XPathExpr
{
return $xpath->addCondition(sprintf(
$value ? 'not(%1$s) or %1$s != %2$s' : '%s != %s',
$attribute,
Translator::getXpathLiteral($value)
));
}
/**
* {@inheritdoc}
*/
public function getName(): string
{
return 'attribute-matching';
}
}

View File

@ -0,0 +1,71 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\XPath\Extension;
use Symfony\Component\CssSelector\XPath\XPathExpr;
/**
* XPath expression translator combination extension.
*
* This component is a port of the Python cssselect library,
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
*
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
*
* @internal
*/
class CombinationExtension extends AbstractExtension
{
/**
* {@inheritdoc}
*/
public function getCombinationTranslators(): array
{
return [
' ' => [$this, 'translateDescendant'],
'>' => [$this, 'translateChild'],
'+' => [$this, 'translateDirectAdjacent'],
'~' => [$this, 'translateIndirectAdjacent'],
];
}
public function translateDescendant(XPathExpr $xpath, XPathExpr $combinedXpath): XPathExpr
{
return $xpath->join('/descendant-or-self::*/', $combinedXpath);
}
public function translateChild(XPathExpr $xpath, XPathExpr $combinedXpath): XPathExpr
{
return $xpath->join('/', $combinedXpath);
}
public function translateDirectAdjacent(XPathExpr $xpath, XPathExpr $combinedXpath): XPathExpr
{
return $xpath
->join('/following-sibling::', $combinedXpath)
->addNameTest()
->addCondition('position() = 1');
}
public function translateIndirectAdjacent(XPathExpr $xpath, XPathExpr $combinedXpath): XPathExpr
{
return $xpath->join('/following-sibling::', $combinedXpath);
}
/**
* {@inheritdoc}
*/
public function getName(): string
{
return 'combination';
}
}

View File

@ -0,0 +1,67 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\XPath\Extension;
/**
* XPath expression translator extension interface.
*
* This component is a port of the Python cssselect library,
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
*
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
*
* @internal
*/
interface ExtensionInterface
{
/**
* Returns node translators.
*
* These callables will receive the node as first argument and the translator as second argument.
*
* @return callable[]
*/
public function getNodeTranslators(): array;
/**
* Returns combination translators.
*
* @return callable[]
*/
public function getCombinationTranslators(): array;
/**
* Returns function translators.
*
* @return callable[]
*/
public function getFunctionTranslators(): array;
/**
* Returns pseudo-class translators.
*
* @return callable[]
*/
public function getPseudoClassTranslators(): array;
/**
* Returns attribute operation translators.
*
* @return callable[]
*/
public function getAttributeMatchingTranslators(): array;
/**
* Returns extension name.
*/
public function getName(): string;
}

View File

@ -0,0 +1,171 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\XPath\Extension;
use Symfony\Component\CssSelector\Exception\ExpressionErrorException;
use Symfony\Component\CssSelector\Exception\SyntaxErrorException;
use Symfony\Component\CssSelector\Node\FunctionNode;
use Symfony\Component\CssSelector\Parser\Parser;
use Symfony\Component\CssSelector\XPath\Translator;
use Symfony\Component\CssSelector\XPath\XPathExpr;
/**
* XPath expression translator function extension.
*
* This component is a port of the Python cssselect library,
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
*
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
*
* @internal
*/
class FunctionExtension extends AbstractExtension
{
/**
* {@inheritdoc}
*/
public function getFunctionTranslators(): array
{
return [
'nth-child' => [$this, 'translateNthChild'],
'nth-last-child' => [$this, 'translateNthLastChild'],
'nth-of-type' => [$this, 'translateNthOfType'],
'nth-last-of-type' => [$this, 'translateNthLastOfType'],
'contains' => [$this, 'translateContains'],
'lang' => [$this, 'translateLang'],
];
}
/**
* @throws ExpressionErrorException
*/
public function translateNthChild(XPathExpr $xpath, FunctionNode $function, bool $last = false, bool $addNameTest = true): XPathExpr
{
try {
[$a, $b] = Parser::parseSeries($function->getArguments());
} catch (SyntaxErrorException $e) {
throw new ExpressionErrorException(sprintf('Invalid series: "%s".', implode('", "', $function->getArguments())), 0, $e);
}
$xpath->addStarPrefix();
if ($addNameTest) {
$xpath->addNameTest();
}
if (0 === $a) {
return $xpath->addCondition('position() = '.($last ? 'last() - '.($b - 1) : $b));
}
if ($a < 0) {
if ($b < 1) {
return $xpath->addCondition('false()');
}
$sign = '<=';
} else {
$sign = '>=';
}
$expr = 'position()';
if ($last) {
$expr = 'last() - '.$expr;
--$b;
}
if (0 !== $b) {
$expr .= ' - '.$b;
}
$conditions = [sprintf('%s %s 0', $expr, $sign)];
if (1 !== $a && -1 !== $a) {
$conditions[] = sprintf('(%s) mod %d = 0', $expr, $a);
}
return $xpath->addCondition(implode(' and ', $conditions));
// todo: handle an+b, odd, even
// an+b means every-a, plus b, e.g., 2n+1 means odd
// 0n+b means b
// n+0 means a=1, i.e., all elements
// an means every a elements, i.e., 2n means even
// -n means -1n
// -1n+6 means elements 6 and previous
}
public function translateNthLastChild(XPathExpr $xpath, FunctionNode $function): XPathExpr
{
return $this->translateNthChild($xpath, $function, true);
}
public function translateNthOfType(XPathExpr $xpath, FunctionNode $function): XPathExpr
{
return $this->translateNthChild($xpath, $function, false, false);
}
/**
* @throws ExpressionErrorException
*/
public function translateNthLastOfType(XPathExpr $xpath, FunctionNode $function): XPathExpr
{
if ('*' === $xpath->getElement()) {
throw new ExpressionErrorException('"*:nth-of-type()" is not implemented.');
}
return $this->translateNthChild($xpath, $function, true, false);
}
/**
* @throws ExpressionErrorException
*/
public function translateContains(XPathExpr $xpath, FunctionNode $function): XPathExpr
{
$arguments = $function->getArguments();
foreach ($arguments as $token) {
if (!($token->isString() || $token->isIdentifier())) {
throw new ExpressionErrorException('Expected a single string or identifier for :contains(), got '.implode(', ', $arguments));
}
}
return $xpath->addCondition(sprintf(
'contains(string(.), %s)',
Translator::getXpathLiteral($arguments[0]->getValue())
));
}
/**
* @throws ExpressionErrorException
*/
public function translateLang(XPathExpr $xpath, FunctionNode $function): XPathExpr
{
$arguments = $function->getArguments();
foreach ($arguments as $token) {
if (!($token->isString() || $token->isIdentifier())) {
throw new ExpressionErrorException('Expected a single string or identifier for :lang(), got '.implode(', ', $arguments));
}
}
return $xpath->addCondition(sprintf(
'lang(%s)',
Translator::getXpathLiteral($arguments[0]->getValue())
));
}
/**
* {@inheritdoc}
*/
public function getName(): string
{
return 'function';
}
}

View File

@ -0,0 +1,187 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\XPath\Extension;
use Symfony\Component\CssSelector\Exception\ExpressionErrorException;
use Symfony\Component\CssSelector\Node\FunctionNode;
use Symfony\Component\CssSelector\XPath\Translator;
use Symfony\Component\CssSelector\XPath\XPathExpr;
/**
* XPath expression translator HTML extension.
*
* This component is a port of the Python cssselect library,
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
*
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
*
* @internal
*/
class HtmlExtension extends AbstractExtension
{
public function __construct(Translator $translator)
{
$translator
->getExtension('node')
->setFlag(NodeExtension::ELEMENT_NAME_IN_LOWER_CASE, true)
->setFlag(NodeExtension::ATTRIBUTE_NAME_IN_LOWER_CASE, true);
}
/**
* {@inheritdoc}
*/
public function getPseudoClassTranslators(): array
{
return [
'checked' => [$this, 'translateChecked'],
'link' => [$this, 'translateLink'],
'disabled' => [$this, 'translateDisabled'],
'enabled' => [$this, 'translateEnabled'],
'selected' => [$this, 'translateSelected'],
'invalid' => [$this, 'translateInvalid'],
'hover' => [$this, 'translateHover'],
'visited' => [$this, 'translateVisited'],
];
}
/**
* {@inheritdoc}
*/
public function getFunctionTranslators(): array
{
return [
'lang' => [$this, 'translateLang'],
];
}
public function translateChecked(XPathExpr $xpath): XPathExpr
{
return $xpath->addCondition(
'(@checked '
."and (name(.) = 'input' or name(.) = 'command')"
."and (@type = 'checkbox' or @type = 'radio'))"
);
}
public function translateLink(XPathExpr $xpath): XPathExpr
{
return $xpath->addCondition("@href and (name(.) = 'a' or name(.) = 'link' or name(.) = 'area')");
}
public function translateDisabled(XPathExpr $xpath): XPathExpr
{
return $xpath->addCondition(
'('
.'@disabled and'
.'('
."(name(.) = 'input' and @type != 'hidden')"
." or name(.) = 'button'"
." or name(.) = 'select'"
." or name(.) = 'textarea'"
." or name(.) = 'command'"
." or name(.) = 'fieldset'"
." or name(.) = 'optgroup'"
." or name(.) = 'option'"
.')'
.') or ('
."(name(.) = 'input' and @type != 'hidden')"
." or name(.) = 'button'"
." or name(.) = 'select'"
." or name(.) = 'textarea'"
.')'
.' and ancestor::fieldset[@disabled]'
);
// todo: in the second half, add "and is not a descendant of that fieldset element's first legend element child, if any."
}
public function translateEnabled(XPathExpr $xpath): XPathExpr
{
return $xpath->addCondition(
'('
.'@href and ('
."name(.) = 'a'"
." or name(.) = 'link'"
." or name(.) = 'area'"
.')'
.') or ('
.'('
."name(.) = 'command'"
." or name(.) = 'fieldset'"
." or name(.) = 'optgroup'"
.')'
.' and not(@disabled)'
.') or ('
.'('
."(name(.) = 'input' and @type != 'hidden')"
." or name(.) = 'button'"
." or name(.) = 'select'"
." or name(.) = 'textarea'"
." or name(.) = 'keygen'"
.')'
.' and not (@disabled or ancestor::fieldset[@disabled])'
.') or ('
."name(.) = 'option' and not("
.'@disabled or ancestor::optgroup[@disabled]'
.')'
.')'
);
}
/**
* @throws ExpressionErrorException
*/
public function translateLang(XPathExpr $xpath, FunctionNode $function): XPathExpr
{
$arguments = $function->getArguments();
foreach ($arguments as $token) {
if (!($token->isString() || $token->isIdentifier())) {
throw new ExpressionErrorException('Expected a single string or identifier for :lang(), got '.implode(', ', $arguments));
}
}
return $xpath->addCondition(sprintf(
'ancestor-or-self::*[@lang][1][starts-with(concat('
."translate(@%s, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), '-')"
.', %s)]',
'lang',
Translator::getXpathLiteral(strtolower($arguments[0]->getValue()).'-')
));
}
public function translateSelected(XPathExpr $xpath): XPathExpr
{
return $xpath->addCondition("(@selected and name(.) = 'option')");
}
public function translateInvalid(XPathExpr $xpath): XPathExpr
{
return $xpath->addCondition('0');
}
public function translateHover(XPathExpr $xpath): XPathExpr
{
return $xpath->addCondition('0');
}
public function translateVisited(XPathExpr $xpath): XPathExpr
{
return $xpath->addCondition('0');
}
/**
* {@inheritdoc}
*/
public function getName(): string
{
return 'html';
}
}

View File

@ -0,0 +1,197 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\XPath\Extension;
use Symfony\Component\CssSelector\Node;
use Symfony\Component\CssSelector\XPath\Translator;
use Symfony\Component\CssSelector\XPath\XPathExpr;
/**
* XPath expression translator node extension.
*
* This component is a port of the Python cssselect library,
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
*
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
*
* @internal
*/
class NodeExtension extends AbstractExtension
{
public const ELEMENT_NAME_IN_LOWER_CASE = 1;
public const ATTRIBUTE_NAME_IN_LOWER_CASE = 2;
public const ATTRIBUTE_VALUE_IN_LOWER_CASE = 4;
private $flags;
public function __construct(int $flags = 0)
{
$this->flags = $flags;
}
/**
* @return $this
*/
public function setFlag(int $flag, bool $on): self
{
if ($on && !$this->hasFlag($flag)) {
$this->flags += $flag;
}
if (!$on && $this->hasFlag($flag)) {
$this->flags -= $flag;
}
return $this;
}
public function hasFlag(int $flag): bool
{
return (bool) ($this->flags & $flag);
}
/**
* {@inheritdoc}
*/
public function getNodeTranslators(): array
{
return [
'Selector' => [$this, 'translateSelector'],
'CombinedSelector' => [$this, 'translateCombinedSelector'],
'Negation' => [$this, 'translateNegation'],
'Function' => [$this, 'translateFunction'],
'Pseudo' => [$this, 'translatePseudo'],
'Attribute' => [$this, 'translateAttribute'],
'Class' => [$this, 'translateClass'],
'Hash' => [$this, 'translateHash'],
'Element' => [$this, 'translateElement'],
];
}
public function translateSelector(Node\SelectorNode $node, Translator $translator): XPathExpr
{
return $translator->nodeToXPath($node->getTree());
}
public function translateCombinedSelector(Node\CombinedSelectorNode $node, Translator $translator): XPathExpr
{
return $translator->addCombination($node->getCombinator(), $node->getSelector(), $node->getSubSelector());
}
public function translateNegation(Node\NegationNode $node, Translator $translator): XPathExpr
{
$xpath = $translator->nodeToXPath($node->getSelector());
$subXpath = $translator->nodeToXPath($node->getSubSelector());
$subXpath->addNameTest();
if ($subXpath->getCondition()) {
return $xpath->addCondition(sprintf('not(%s)', $subXpath->getCondition()));
}
return $xpath->addCondition('0');
}
public function translateFunction(Node\FunctionNode $node, Translator $translator): XPathExpr
{
$xpath = $translator->nodeToXPath($node->getSelector());
return $translator->addFunction($xpath, $node);
}
public function translatePseudo(Node\PseudoNode $node, Translator $translator): XPathExpr
{
$xpath = $translator->nodeToXPath($node->getSelector());
return $translator->addPseudoClass($xpath, $node->getIdentifier());
}
public function translateAttribute(Node\AttributeNode $node, Translator $translator): XPathExpr
{
$name = $node->getAttribute();
$safe = $this->isSafeName($name);
if ($this->hasFlag(self::ATTRIBUTE_NAME_IN_LOWER_CASE)) {
$name = strtolower($name);
}
if ($node->getNamespace()) {
$name = sprintf('%s:%s', $node->getNamespace(), $name);
$safe = $safe && $this->isSafeName($node->getNamespace());
}
$attribute = $safe ? '@'.$name : sprintf('attribute::*[name() = %s]', Translator::getXpathLiteral($name));
$value = $node->getValue();
$xpath = $translator->nodeToXPath($node->getSelector());
if ($this->hasFlag(self::ATTRIBUTE_VALUE_IN_LOWER_CASE)) {
$value = strtolower($value);
}
return $translator->addAttributeMatching($xpath, $node->getOperator(), $attribute, $value);
}
public function translateClass(Node\ClassNode $node, Translator $translator): XPathExpr
{
$xpath = $translator->nodeToXPath($node->getSelector());
return $translator->addAttributeMatching($xpath, '~=', '@class', $node->getName());
}
public function translateHash(Node\HashNode $node, Translator $translator): XPathExpr
{
$xpath = $translator->nodeToXPath($node->getSelector());
return $translator->addAttributeMatching($xpath, '=', '@id', $node->getId());
}
public function translateElement(Node\ElementNode $node): XPathExpr
{
$element = $node->getElement();
if ($element && $this->hasFlag(self::ELEMENT_NAME_IN_LOWER_CASE)) {
$element = strtolower($element);
}
if ($element) {
$safe = $this->isSafeName($element);
} else {
$element = '*';
$safe = true;
}
if ($node->getNamespace()) {
$element = sprintf('%s:%s', $node->getNamespace(), $element);
$safe = $safe && $this->isSafeName($node->getNamespace());
}
$xpath = new XPathExpr('', $element);
if (!$safe) {
$xpath->addNameTest();
}
return $xpath;
}
/**
* {@inheritdoc}
*/
public function getName(): string
{
return 'node';
}
private function isSafeName(string $name): bool
{
return 0 < preg_match('~^[a-zA-Z_][a-zA-Z0-9_.-]*$~', $name);
}
}

View File

@ -0,0 +1,122 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\XPath\Extension;
use Symfony\Component\CssSelector\Exception\ExpressionErrorException;
use Symfony\Component\CssSelector\XPath\XPathExpr;
/**
* XPath expression translator pseudo-class extension.
*
* This component is a port of the Python cssselect library,
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
*
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
*
* @internal
*/
class PseudoClassExtension extends AbstractExtension
{
/**
* {@inheritdoc}
*/
public function getPseudoClassTranslators(): array
{
return [
'root' => [$this, 'translateRoot'],
'first-child' => [$this, 'translateFirstChild'],
'last-child' => [$this, 'translateLastChild'],
'first-of-type' => [$this, 'translateFirstOfType'],
'last-of-type' => [$this, 'translateLastOfType'],
'only-child' => [$this, 'translateOnlyChild'],
'only-of-type' => [$this, 'translateOnlyOfType'],
'empty' => [$this, 'translateEmpty'],
];
}
public function translateRoot(XPathExpr $xpath): XPathExpr
{
return $xpath->addCondition('not(parent::*)');
}
public function translateFirstChild(XPathExpr $xpath): XPathExpr
{
return $xpath
->addStarPrefix()
->addNameTest()
->addCondition('position() = 1');
}
public function translateLastChild(XPathExpr $xpath): XPathExpr
{
return $xpath
->addStarPrefix()
->addNameTest()
->addCondition('position() = last()');
}
/**
* @throws ExpressionErrorException
*/
public function translateFirstOfType(XPathExpr $xpath): XPathExpr
{
if ('*' === $xpath->getElement()) {
throw new ExpressionErrorException('"*:first-of-type" is not implemented.');
}
return $xpath
->addStarPrefix()
->addCondition('position() = 1');
}
/**
* @throws ExpressionErrorException
*/
public function translateLastOfType(XPathExpr $xpath): XPathExpr
{
if ('*' === $xpath->getElement()) {
throw new ExpressionErrorException('"*:last-of-type" is not implemented.');
}
return $xpath
->addStarPrefix()
->addCondition('position() = last()');
}
public function translateOnlyChild(XPathExpr $xpath): XPathExpr
{
return $xpath
->addStarPrefix()
->addNameTest()
->addCondition('last() = 1');
}
public function translateOnlyOfType(XPathExpr $xpath): XPathExpr
{
$element = $xpath->getElement();
return $xpath->addCondition(sprintf('count(preceding-sibling::%s)=0 and count(following-sibling::%s)=0', $element, $element));
}
public function translateEmpty(XPathExpr $xpath): XPathExpr
{
return $xpath->addCondition('not(*) and not(string-length())');
}
/**
* {@inheritdoc}
*/
public function getName(): string
{
return 'pseudo-class';
}
}

View File

@ -0,0 +1,230 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\XPath;
use Symfony\Component\CssSelector\Exception\ExpressionErrorException;
use Symfony\Component\CssSelector\Node\FunctionNode;
use Symfony\Component\CssSelector\Node\NodeInterface;
use Symfony\Component\CssSelector\Node\SelectorNode;
use Symfony\Component\CssSelector\Parser\Parser;
use Symfony\Component\CssSelector\Parser\ParserInterface;
/**
* XPath expression translator interface.
*
* This component is a port of the Python cssselect library,
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
*
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
*
* @internal
*/
class Translator implements TranslatorInterface
{
private $mainParser;
/**
* @var ParserInterface[]
*/
private $shortcutParsers = [];
/**
* @var Extension\ExtensionInterface[]
*/
private $extensions = [];
private $nodeTranslators = [];
private $combinationTranslators = [];
private $functionTranslators = [];
private $pseudoClassTranslators = [];
private $attributeMatchingTranslators = [];
public function __construct(ParserInterface $parser = null)
{
$this->mainParser = $parser ?? new Parser();
$this
->registerExtension(new Extension\NodeExtension())
->registerExtension(new Extension\CombinationExtension())
->registerExtension(new Extension\FunctionExtension())
->registerExtension(new Extension\PseudoClassExtension())
->registerExtension(new Extension\AttributeMatchingExtension())
;
}
public static function getXpathLiteral(string $element): string
{
if (!str_contains($element, "'")) {
return "'".$element."'";
}
if (!str_contains($element, '"')) {
return '"'.$element.'"';
}
$string = $element;
$parts = [];
while (true) {
if (false !== $pos = strpos($string, "'")) {
$parts[] = sprintf("'%s'", substr($string, 0, $pos));
$parts[] = "\"'\"";
$string = substr($string, $pos + 1);
} else {
$parts[] = "'$string'";
break;
}
}
return sprintf('concat(%s)', implode(', ', $parts));
}
/**
* {@inheritdoc}
*/
public function cssToXPath(string $cssExpr, string $prefix = 'descendant-or-self::'): string
{
$selectors = $this->parseSelectors($cssExpr);
/** @var SelectorNode $selector */
foreach ($selectors as $index => $selector) {
if (null !== $selector->getPseudoElement()) {
throw new ExpressionErrorException('Pseudo-elements are not supported.');
}
$selectors[$index] = $this->selectorToXPath($selector, $prefix);
}
return implode(' | ', $selectors);
}
/**
* {@inheritdoc}
*/
public function selectorToXPath(SelectorNode $selector, string $prefix = 'descendant-or-self::'): string
{
return ($prefix ?: '').$this->nodeToXPath($selector);
}
/**
* @return $this
*/
public function registerExtension(Extension\ExtensionInterface $extension): self
{
$this->extensions[$extension->getName()] = $extension;
$this->nodeTranslators = array_merge($this->nodeTranslators, $extension->getNodeTranslators());
$this->combinationTranslators = array_merge($this->combinationTranslators, $extension->getCombinationTranslators());
$this->functionTranslators = array_merge($this->functionTranslators, $extension->getFunctionTranslators());
$this->pseudoClassTranslators = array_merge($this->pseudoClassTranslators, $extension->getPseudoClassTranslators());
$this->attributeMatchingTranslators = array_merge($this->attributeMatchingTranslators, $extension->getAttributeMatchingTranslators());
return $this;
}
/**
* @throws ExpressionErrorException
*/
public function getExtension(string $name): Extension\ExtensionInterface
{
if (!isset($this->extensions[$name])) {
throw new ExpressionErrorException(sprintf('Extension "%s" not registered.', $name));
}
return $this->extensions[$name];
}
/**
* @return $this
*/
public function registerParserShortcut(ParserInterface $shortcut): self
{
$this->shortcutParsers[] = $shortcut;
return $this;
}
/**
* @throws ExpressionErrorException
*/
public function nodeToXPath(NodeInterface $node): XPathExpr
{
if (!isset($this->nodeTranslators[$node->getNodeName()])) {
throw new ExpressionErrorException(sprintf('Node "%s" not supported.', $node->getNodeName()));
}
return $this->nodeTranslators[$node->getNodeName()]($node, $this);
}
/**
* @throws ExpressionErrorException
*/
public function addCombination(string $combiner, NodeInterface $xpath, NodeInterface $combinedXpath): XPathExpr
{
if (!isset($this->combinationTranslators[$combiner])) {
throw new ExpressionErrorException(sprintf('Combiner "%s" not supported.', $combiner));
}
return $this->combinationTranslators[$combiner]($this->nodeToXPath($xpath), $this->nodeToXPath($combinedXpath));
}
/**
* @throws ExpressionErrorException
*/
public function addFunction(XPathExpr $xpath, FunctionNode $function): XPathExpr
{
if (!isset($this->functionTranslators[$function->getName()])) {
throw new ExpressionErrorException(sprintf('Function "%s" not supported.', $function->getName()));
}
return $this->functionTranslators[$function->getName()]($xpath, $function);
}
/**
* @throws ExpressionErrorException
*/
public function addPseudoClass(XPathExpr $xpath, string $pseudoClass): XPathExpr
{
if (!isset($this->pseudoClassTranslators[$pseudoClass])) {
throw new ExpressionErrorException(sprintf('Pseudo-class "%s" not supported.', $pseudoClass));
}
return $this->pseudoClassTranslators[$pseudoClass]($xpath);
}
/**
* @throws ExpressionErrorException
*/
public function addAttributeMatching(XPathExpr $xpath, string $operator, string $attribute, $value): XPathExpr
{
if (!isset($this->attributeMatchingTranslators[$operator])) {
throw new ExpressionErrorException(sprintf('Attribute matcher operator "%s" not supported.', $operator));
}
return $this->attributeMatchingTranslators[$operator]($xpath, $attribute, $value);
}
/**
* @return SelectorNode[]
*/
private function parseSelectors(string $css): array
{
foreach ($this->shortcutParsers as $shortcut) {
$tokens = $shortcut->parse($css);
if (!empty($tokens)) {
return $tokens;
}
}
return $this->mainParser->parse($css);
}
}

View File

@ -0,0 +1,37 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\XPath;
use Symfony\Component\CssSelector\Node\SelectorNode;
/**
* XPath expression translator interface.
*
* This component is a port of the Python cssselect library,
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
*
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
*
* @internal
*/
interface TranslatorInterface
{
/**
* Translates a CSS selector to an XPath expression.
*/
public function cssToXPath(string $cssExpr, string $prefix = 'descendant-or-self::'): string;
/**
* Translates a parsed selector node to an XPath expression.
*/
public function selectorToXPath(SelectorNode $selector, string $prefix = 'descendant-or-self::'): string;
}

View File

@ -0,0 +1,102 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\XPath;
/**
* XPath expression translator interface.
*
* This component is a port of the Python cssselect library,
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
*
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
*
* @internal
*/
class XPathExpr
{
private $path;
private $element;
private $condition;
public function __construct(string $path = '', string $element = '*', string $condition = '', bool $starPrefix = false)
{
$this->path = $path;
$this->element = $element;
$this->condition = $condition;
if ($starPrefix) {
$this->addStarPrefix();
}
}
public function getElement(): string
{
return $this->element;
}
public function addCondition(string $condition): self
{
$this->condition = $this->condition ? sprintf('(%s) and (%s)', $this->condition, $condition) : $condition;
return $this;
}
public function getCondition(): string
{
return $this->condition;
}
public function addNameTest(): self
{
if ('*' !== $this->element) {
$this->addCondition('name() = '.Translator::getXpathLiteral($this->element));
$this->element = '*';
}
return $this;
}
public function addStarPrefix(): self
{
$this->path .= '*/';
return $this;
}
/**
* Joins another XPathExpr with a combiner.
*
* @return $this
*/
public function join(string $combiner, self $expr): self
{
$path = $this->__toString().$combiner;
if ('*/' !== $expr->path) {
$path .= $expr->path;
}
$this->path = $path;
$this->element = $expr->element;
$this->condition = $expr->condition;
return $this;
}
public function __toString(): string
{
$path = $this->path.$this->element;
$condition = null === $this->condition || '' === $this->condition ? '' : '['.$this->condition.']';
return $path.$condition;
}
}

View File

@ -0,0 +1,33 @@
{
"name": "symfony/css-selector",
"type": "library",
"description": "Converts CSS selectors to XPath expressions",
"keywords": [],
"homepage": "https://symfony.com",
"license": "MIT",
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Jean-François Simon",
"email": "jeanfrancois.simon@sensiolabs.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"require": {
"php": ">=7.1.3",
"symfony/polyfill-php80": "^1.16"
},
"autoload": {
"psr-4": { "Symfony\\Component\\CssSelector\\": "" },
"exclude-from-classmap": [
"/Tests/"
]
},
"minimum-stability": "dev"
}

View File

@ -0,0 +1,19 @@
Copyright (c) 2020 Fabien Potencier
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@ -0,0 +1,115 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Polyfill\Php80;
/**
* @author Ion Bazan <ion.bazan@gmail.com>
* @author Nico Oelgart <nicoswd@gmail.com>
* @author Nicolas Grekas <p@tchwork.com>
*
* @internal
*/
final class Php80
{
public static function fdiv(float $dividend, float $divisor): float
{
return @($dividend / $divisor);
}
public static function get_debug_type($value): string
{
switch (true) {
case null === $value: return 'null';
case \is_bool($value): return 'bool';
case \is_string($value): return 'string';
case \is_array($value): return 'array';
case \is_int($value): return 'int';
case \is_float($value): return 'float';
case \is_object($value): break;
case $value instanceof \__PHP_Incomplete_Class: return '__PHP_Incomplete_Class';
default:
if (null === $type = @get_resource_type($value)) {
return 'unknown';
}
if ('Unknown' === $type) {
$type = 'closed';
}
return "resource ($type)";
}
$class = \get_class($value);
if (false === strpos($class, '@')) {
return $class;
}
return (get_parent_class($class) ?: key(class_implements($class)) ?: 'class').'@anonymous';
}
public static function get_resource_id($res): int
{
if (!\is_resource($res) && null === @get_resource_type($res)) {
throw new \TypeError(sprintf('Argument 1 passed to get_resource_id() must be of the type resource, %s given', get_debug_type($res)));
}
return (int) $res;
}
public static function preg_last_error_msg(): string
{
switch (preg_last_error()) {
case \PREG_INTERNAL_ERROR:
return 'Internal error';
case \PREG_BAD_UTF8_ERROR:
return 'Malformed UTF-8 characters, possibly incorrectly encoded';
case \PREG_BAD_UTF8_OFFSET_ERROR:
return 'The offset did not correspond to the beginning of a valid UTF-8 code point';
case \PREG_BACKTRACK_LIMIT_ERROR:
return 'Backtrack limit exhausted';
case \PREG_RECURSION_LIMIT_ERROR:
return 'Recursion limit exhausted';
case \PREG_JIT_STACKLIMIT_ERROR:
return 'JIT stack limit exhausted';
case \PREG_NO_ERROR:
return 'No error';
default:
return 'Unknown error';
}
}
public static function str_contains(string $haystack, string $needle): bool
{
return '' === $needle || false !== strpos($haystack, $needle);
}
public static function str_starts_with(string $haystack, string $needle): bool
{
return 0 === strncmp($haystack, $needle, \strlen($needle));
}
public static function str_ends_with(string $haystack, string $needle): bool
{
if ('' === $needle || $needle === $haystack) {
return true;
}
if ('' === $haystack) {
return false;
}
$needleLength = \strlen($needle);
return $needleLength <= \strlen($haystack) && 0 === substr_compare($haystack, $needle, -$needleLength);
}
}

View File

@ -0,0 +1,103 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Polyfill\Php80;
/**
* @author Fedonyuk Anton <info@ensostudio.ru>
*
* @internal
*/
class PhpToken implements \Stringable
{
/**
* @var int
*/
public $id;
/**
* @var string
*/
public $text;
/**
* @var int
*/
public $line;
/**
* @var int
*/
public $pos;
public function __construct(int $id, string $text, int $line = -1, int $position = -1)
{
$this->id = $id;
$this->text = $text;
$this->line = $line;
$this->pos = $position;
}
public function getTokenName(): ?string
{
if ('UNKNOWN' === $name = token_name($this->id)) {
$name = \strlen($this->text) > 1 || \ord($this->text) < 32 ? null : $this->text;
}
return $name;
}
/**
* @param int|string|array $kind
*/
public function is($kind): bool
{
foreach ((array) $kind as $value) {
if (\in_array($value, [$this->id, $this->text], true)) {
return true;
}
}
return false;
}
public function isIgnorable(): bool
{
return \in_array($this->id, [\T_WHITESPACE, \T_COMMENT, \T_DOC_COMMENT, \T_OPEN_TAG], true);
}
public function __toString(): string
{
return (string) $this->text;
}
/**
* @return static[]
*/
public static function tokenize(string $code, int $flags = 0): array
{
$line = 1;
$position = 0;
$tokens = token_get_all($code, $flags);
foreach ($tokens as $index => $token) {
if (\is_string($token)) {
$id = \ord($token);
$text = $token;
} else {
[$id, $text, $line] = $token;
}
$tokens[$index] = new static($id, $text, $line, $position);
$position += \strlen($text);
}
return $tokens;
}
}

View File

@ -0,0 +1,25 @@
Symfony Polyfill / Php80
========================
This component provides features added to PHP 8.0 core:
- [`Stringable`](https://php.net/stringable) interface
- [`fdiv`](https://php.net/fdiv)
- [`ValueError`](https://php.net/valueerror) class
- [`UnhandledMatchError`](https://php.net/unhandledmatcherror) class
- `FILTER_VALIDATE_BOOL` constant
- [`get_debug_type`](https://php.net/get_debug_type)
- [`PhpToken`](https://php.net/phptoken) class
- [`preg_last_error_msg`](https://php.net/preg_last_error_msg)
- [`str_contains`](https://php.net/str_contains)
- [`str_starts_with`](https://php.net/str_starts_with)
- [`str_ends_with`](https://php.net/str_ends_with)
- [`get_resource_id`](https://php.net/get_resource_id)
More information can be found in the
[main Polyfill README](https://github.com/symfony/polyfill/blob/main/README.md).
License
=======
This library is released under the [MIT license](LICENSE).

View File

@ -0,0 +1,22 @@
<?php
#[Attribute(Attribute::TARGET_CLASS)]
final class Attribute
{
public const TARGET_CLASS = 1;
public const TARGET_FUNCTION = 2;
public const TARGET_METHOD = 4;
public const TARGET_PROPERTY = 8;
public const TARGET_CLASS_CONSTANT = 16;
public const TARGET_PARAMETER = 32;
public const TARGET_ALL = 63;
public const IS_REPEATABLE = 64;
/** @var int */
public $flags;
public function __construct(int $flags = self::TARGET_ALL)
{
$this->flags = $flags;
}
}

Some files were not shown because too many files have changed in this diff Show More