Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/nextcloud/3rdparty.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJulius Härtl <jus@bitgrid.net>2022-08-30 12:56:14 +0300
committerGitHub <noreply@github.com>2022-08-30 12:56:14 +0300
commitd6a35b6d5759c08dd268618951f9e5b1c18aa939 (patch)
treeb89067be52749a3d6294402188394ca087cb7415
parent020d0d3892bd3b7296db8ed21448c834d33d5723 (diff)
parent0ddbaa9bc4faded2f5a580f7ba2022d66ce7bd17 (diff)
Merge pull request #1131 from nextcloud/deps/opengraphv25.0.0beta4
-rw-r--r--.gitignore14
-rw-r--r--composer.json3
-rw-r--r--composer.lock249
-rw-r--r--composer/autoload_classmap.php81
-rw-r--r--composer/autoload_files.php8
-rw-r--r--composer/autoload_namespaces.php1
-rw-r--r--composer/autoload_psr4.php3
-rw-r--r--composer/autoload_static.php111
-rw-r--r--composer/installed.json257
-rw-r--r--composer/installed.php40
-rw-r--r--composer/platform_check.php4
-rw-r--r--fusonic/linq/LICENSE20
-rw-r--r--fusonic/linq/src/Fusonic/Linq/GroupedLinq.php35
-rw-r--r--fusonic/linq/src/Fusonic/Linq/Helper/LinqHelper.php67
-rw-r--r--fusonic/linq/src/Fusonic/Linq/Iterator/DistinctIterator.php62
-rw-r--r--fusonic/linq/src/Fusonic/Linq/Iterator/ExceptIterator.php65
-rw-r--r--fusonic/linq/src/Fusonic/Linq/Iterator/GroupIterator.php74
-rw-r--r--fusonic/linq/src/Fusonic/Linq/Iterator/IntersectIterator.php65
-rw-r--r--fusonic/linq/src/Fusonic/Linq/Iterator/OfTypeIterator.php94
-rw-r--r--fusonic/linq/src/Fusonic/Linq/Iterator/OrderIterator.php113
-rw-r--r--fusonic/linq/src/Fusonic/Linq/Iterator/SelectIterator.php37
-rw-r--r--fusonic/linq/src/Fusonic/Linq/Iterator/SelectManyIterator.php84
-rw-r--r--fusonic/linq/src/Fusonic/Linq/Iterator/WhereIterator.php34
-rw-r--r--fusonic/linq/src/Fusonic/Linq/Linq.php732
-rw-r--r--fusonic/opengraph/LICENSE20
-rw-r--r--fusonic/opengraph/src/Consumer.php156
-rw-r--r--fusonic/opengraph/src/Elements/Audio.php67
-rw-r--r--fusonic/opengraph/src/Elements/ElementBase.php20
-rw-r--r--fusonic/opengraph/src/Elements/Image.php100
-rw-r--r--fusonic/opengraph/src/Elements/Video.php89
-rw-r--r--fusonic/opengraph/src/Objects/ObjectBase.php358
-rw-r--r--fusonic/opengraph/src/Objects/Website.php21
-rw-r--r--fusonic/opengraph/src/Property.php54
-rw-r--r--fusonic/opengraph/src/Publisher.php65
-rw-r--r--symfony/css-selector/CssSelectorConverter.php69
-rw-r--r--symfony/css-selector/Exception/ExceptionInterface.php24
-rw-r--r--symfony/css-selector/Exception/ExpressionErrorException.php24
-rw-r--r--symfony/css-selector/Exception/InternalErrorException.php24
-rw-r--r--symfony/css-selector/Exception/ParseException.php24
-rw-r--r--symfony/css-selector/Exception/SyntaxErrorException.php65
-rw-r--r--symfony/css-selector/LICENSE19
-rw-r--r--symfony/css-selector/Node/AbstractNode.php39
-rw-r--r--symfony/css-selector/Node/AttributeNode.php82
-rw-r--r--symfony/css-selector/Node/ClassNode.php57
-rw-r--r--symfony/css-selector/Node/CombinedSelectorNode.php66
-rw-r--r--symfony/css-selector/Node/ElementNode.php59
-rw-r--r--symfony/css-selector/Node/FunctionNode.php76
-rw-r--r--symfony/css-selector/Node/HashNode.php57
-rw-r--r--symfony/css-selector/Node/NegationNode.php57
-rw-r--r--symfony/css-selector/Node/NodeInterface.php31
-rw-r--r--symfony/css-selector/Node/PseudoNode.php57
-rw-r--r--symfony/css-selector/Node/SelectorNode.php57
-rw-r--r--symfony/css-selector/Node/Specificity.php73
-rw-r--r--symfony/css-selector/Parser/Handler/CommentHandler.php48
-rw-r--r--symfony/css-selector/Parser/Handler/HandlerInterface.php30
-rw-r--r--symfony/css-selector/Parser/Handler/HashHandler.php58
-rw-r--r--symfony/css-selector/Parser/Handler/IdentifierHandler.php58
-rw-r--r--symfony/css-selector/Parser/Handler/NumberHandler.php54
-rw-r--r--symfony/css-selector/Parser/Handler/StringHandler.php77
-rw-r--r--symfony/css-selector/Parser/Handler/WhitespaceHandler.php46
-rw-r--r--symfony/css-selector/Parser/Parser.php353
-rw-r--r--symfony/css-selector/Parser/ParserInterface.php34
-rw-r--r--symfony/css-selector/Parser/Reader.php86
-rw-r--r--symfony/css-selector/Parser/Shortcut/ClassParser.php51
-rw-r--r--symfony/css-selector/Parser/Shortcut/ElementParser.php47
-rw-r--r--symfony/css-selector/Parser/Shortcut/EmptyStringParser.php46
-rw-r--r--symfony/css-selector/Parser/Shortcut/HashParser.php51
-rw-r--r--symfony/css-selector/Parser/Token.php111
-rw-r--r--symfony/css-selector/Parser/TokenStream.php167
-rw-r--r--symfony/css-selector/Parser/Tokenizer/Tokenizer.php73
-rw-r--r--symfony/css-selector/Parser/Tokenizer/TokenizerEscaping.php65
-rw-r--r--symfony/css-selector/Parser/Tokenizer/TokenizerPatterns.php89
-rw-r--r--symfony/css-selector/README.md20
-rw-r--r--symfony/css-selector/XPath/Extension/AbstractExtension.php65
-rw-r--r--symfony/css-selector/XPath/Extension/AttributeMatchingExtension.php119
-rw-r--r--symfony/css-selector/XPath/Extension/CombinationExtension.php71
-rw-r--r--symfony/css-selector/XPath/Extension/ExtensionInterface.php67
-rw-r--r--symfony/css-selector/XPath/Extension/FunctionExtension.php171
-rw-r--r--symfony/css-selector/XPath/Extension/HtmlExtension.php187
-rw-r--r--symfony/css-selector/XPath/Extension/NodeExtension.php197
-rw-r--r--symfony/css-selector/XPath/Extension/PseudoClassExtension.php122
-rw-r--r--symfony/css-selector/XPath/Translator.php230
-rw-r--r--symfony/css-selector/XPath/TranslatorInterface.php37
-rw-r--r--symfony/css-selector/XPath/XPathExpr.php111
-rw-r--r--symfony/css-selector/composer.json33
-rw-r--r--symfony/dom-crawler/AbstractUriElement.php131
-rw-r--r--symfony/dom-crawler/Crawler.php1309
-rw-r--r--symfony/dom-crawler/Field/ChoiceFormField.php321
-rw-r--r--symfony/dom-crawler/Field/FileFormField.php102
-rw-r--r--symfony/dom-crawler/Field/FormField.php131
-rw-r--r--symfony/dom-crawler/Field/InputFormField.php46
-rw-r--r--symfony/dom-crawler/Field/TextareaFormField.php37
-rw-r--r--symfony/dom-crawler/Form.php501
-rw-r--r--symfony/dom-crawler/FormFieldRegistry.php178
-rw-r--r--symfony/dom-crawler/Image.php37
-rw-r--r--symfony/dom-crawler/LICENSE19
-rw-r--r--symfony/dom-crawler/Link.php34
-rw-r--r--symfony/dom-crawler/README.md13
-rw-r--r--symfony/dom-crawler/UriResolver.php136
-rw-r--r--symfony/dom-crawler/composer.json42
100 files changed, 10159 insertions, 15 deletions
diff --git a/.gitignore b/.gitignore
index 42e772fa..d9610862 100644
--- a/.gitignore
+++ b/.gitignore
@@ -58,6 +58,20 @@ doctrine/inflector/tests
doctrine/lexer/composer.json
doctrine/lexer/LICENSE
+fusonic/linq/examples/
+fusonic/linq/tests/
+fusonic/linq/composer.json
+fusonic/linq/.gitignore
+fusonic/linq/README.md
+
+fusonic/opengraph/examples/
+fusonic/opengraph/.gitattributes
+fusonic/opengraph/.gitignore
+fusonic/opengraph/.scrutinizer.yml
+fusonic/opengraph/composer.json
+fusonic/opengraph/phpunit.xml
+fusonic/opengraph/README.md
+
giggsey/libphonenumber-for-php/METADATA-VERSION.txt
giggsey/locale/CLDR-VERSION.txt
diff --git a/composer.json b/composer.json
index e291fb3a..0cc064d8 100644
--- a/composer.json
+++ b/composer.json
@@ -8,7 +8,7 @@
"optimize-autoloader": true,
"classmap-authoritative": true,
"platform": {
- "php": "7.3.0"
+ "php": "7.4.0"
},
"sort-packages": true,
"allow-plugins": {
@@ -25,6 +25,7 @@
"deepdiver1975/tarstreamer": "v2.0.0",
"doctrine/dbal": "3.1.4",
"egulias/email-validator": "3.1.1",
+ "fusonic/opengraph": "^2.2",
"giggsey/libphonenumber-for-php": "^8.12.37",
"guzzlehttp/guzzle": "^7.4.0",
"icewind/searchdav": "^3.0",
diff --git a/composer.lock b/composer.lock
index 2e4479b4..9a646905 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "62a9ea5f04f28df33dfdeea90ed8bcdb",
+ "content-hash": "2297310d6ca23ae439e3c5d2642b5331",
"packages": [
{
"name": "aws/aws-sdk-php",
@@ -1090,6 +1090,110 @@
"time": "2021-04-24T19:01:55+00:00"
},
{
+ "name": "fusonic/linq",
+ "version": "v1.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/fusonic/linq.git",
+ "reference": "63520ef1470ca771acbd26871efb945dd4a7a5d8"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/fusonic/linq/zipball/63520ef1470ca771acbd26871efb945dd4a7a5d8",
+ "reference": "63520ef1470ca771acbd26871efb945dd4a7a5d8",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.2"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-0": {
+ "Fusonic\\Linq": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fusonic",
+ "homepage": "http://www.fusonic.net"
+ }
+ ],
+ "description": "LINQ 2 objects class for PHP",
+ "homepage": "http://fusonic.github.io/fusonic-linq/",
+ "keywords": [
+ "linq",
+ "linq2objects"
+ ],
+ "support": {
+ "issues": "https://github.com/fusonic/linq/issues",
+ "source": "https://github.com/fusonic/linq/tree/master"
+ },
+ "time": "2015-02-26T22:49:17+00:00"
+ },
+ {
+ "name": "fusonic/opengraph",
+ "version": "v2.2.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/fusonic/opengraph.git",
+ "reference": "a63b588fbe56c175ae06e158f1513642653ee3c1"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/fusonic/opengraph/zipball/a63b588fbe56c175ae06e158f1513642653ee3c1",
+ "reference": "a63b588fbe56c175ae06e158f1513642653ee3c1",
+ "shasum": ""
+ },
+ "require": {
+ "ext-dom": "*",
+ "fusonic/linq": "^1.0",
+ "php": "^7.4|^8.0",
+ "psr/http-client": "^1.0",
+ "psr/http-factory": "^1.0",
+ "symfony/css-selector": "^3.0|^4.0|^5.0|^6.0",
+ "symfony/dom-crawler": "^3.0|^4.0|^5.0|^6.0"
+ },
+ "require-dev": {
+ "nyholm/psr7": "^1.2",
+ "phpunit/phpunit": "^9.0",
+ "symfony/http-client": "^6.0"
+ },
+ "suggest": {
+ "nyholm/psr7": "^1.2",
+ "symfony/http-client": "^5.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Fusonic\\OpenGraph\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fusonic",
+ "homepage": "https://www.fusonic.net"
+ }
+ ],
+ "description": "PHP library for consuming and publishing Open Graph resources.",
+ "homepage": "https://github.com/fusonic/fusonic-opengraph",
+ "keywords": [
+ "opengraph"
+ ],
+ "support": {
+ "issues": "https://github.com/fusonic/opengraph/issues",
+ "source": "https://github.com/fusonic/opengraph/tree/v2.2.0"
+ },
+ "time": "2022-01-20T05:47:36+00:00"
+ },
+ {
"name": "giggsey/libphonenumber-for-php",
"version": "8.12.38",
"source": {
@@ -4520,6 +4624,72 @@
"time": "2021-08-25T19:27:26+00:00"
},
{
+ "name": "symfony/css-selector",
+ "version": "v5.4.11",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/css-selector.git",
+ "reference": "c1681789f059ab756001052164726ae88512ae3d"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/css-selector/zipball/c1681789f059ab756001052164726ae88512ae3d",
+ "reference": "c1681789f059ab756001052164726ae88512ae3d",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2.5",
+ "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/v5.4.11"
+ },
+ "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-27T16:58:25+00:00"
+ },
+ {
"name": "symfony/deprecation-contracts",
"version": "v2.5.2",
"source": {
@@ -4587,6 +4757,81 @@
"time": "2022-01-02T09:53:40+00:00"
},
{
+ "name": "symfony/dom-crawler",
+ "version": "v5.4.11",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/dom-crawler.git",
+ "reference": "0b900ca5576ecd59e08c76127e616667cfe427a7"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/0b900ca5576ecd59e08c76127e616667cfe427a7",
+ "reference": "0b900ca5576ecd59e08c76127e616667cfe427a7",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2.5",
+ "symfony/deprecation-contracts": "^2.1|^3",
+ "symfony/polyfill-ctype": "~1.8",
+ "symfony/polyfill-mbstring": "~1.0",
+ "symfony/polyfill-php80": "^1.16"
+ },
+ "conflict": {
+ "masterminds/html5": "<2.6"
+ },
+ "require-dev": {
+ "masterminds/html5": "^2.6",
+ "symfony/css-selector": "^4.4|^5.0|^6.0"
+ },
+ "suggest": {
+ "symfony/css-selector": ""
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\DomCrawler\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Eases DOM navigation for HTML and XML documents",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/dom-crawler/tree/v5.4.11"
+ },
+ "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-27T16:58:25+00:00"
+ },
+ {
"name": "symfony/event-dispatcher",
"version": "v4.4.30",
"source": {
@@ -6316,7 +6561,7 @@
},
"platform-dev": [],
"platform-overrides": {
- "php": "7.3.0"
+ "php": "7.4.0"
},
"plugin-api-version": "2.3.0"
}
diff --git a/composer/autoload_classmap.php b/composer/autoload_classmap.php
index d67ffe5b..ad99ca64 100644
--- a/composer/autoload_classmap.php
+++ b/composer/autoload_classmap.php
@@ -1333,6 +1333,27 @@ return array(
'FG\\X509\\SAN\\DNSName' => $vendorDir . '/fgrosse/phpasn1/lib/X509/SAN/DNSName.php',
'FG\\X509\\SAN\\IPAddress' => $vendorDir . '/fgrosse/phpasn1/lib/X509/SAN/IPAddress.php',
'FG\\X509\\SAN\\SubjectAlternativeNames' => $vendorDir . '/fgrosse/phpasn1/lib/X509/SAN/SubjectAlternativeNames.php',
+ 'Fusonic\\Linq\\GroupedLinq' => $vendorDir . '/fusonic/linq/src/Fusonic/Linq/GroupedLinq.php',
+ 'Fusonic\\Linq\\Helper\\LinqHelper' => $vendorDir . '/fusonic/linq/src/Fusonic/Linq/Helper/LinqHelper.php',
+ 'Fusonic\\Linq\\Iterator\\DistinctIterator' => $vendorDir . '/fusonic/linq/src/Fusonic/Linq/Iterator/DistinctIterator.php',
+ 'Fusonic\\Linq\\Iterator\\ExceptIterator' => $vendorDir . '/fusonic/linq/src/Fusonic/Linq/Iterator/ExceptIterator.php',
+ 'Fusonic\\Linq\\Iterator\\GroupIterator' => $vendorDir . '/fusonic/linq/src/Fusonic/Linq/Iterator/GroupIterator.php',
+ 'Fusonic\\Linq\\Iterator\\IntersectIterator' => $vendorDir . '/fusonic/linq/src/Fusonic/Linq/Iterator/IntersectIterator.php',
+ 'Fusonic\\Linq\\Iterator\\OfTypeIterator' => $vendorDir . '/fusonic/linq/src/Fusonic/Linq/Iterator/OfTypeIterator.php',
+ 'Fusonic\\Linq\\Iterator\\OrderIterator' => $vendorDir . '/fusonic/linq/src/Fusonic/Linq/Iterator/OrderIterator.php',
+ 'Fusonic\\Linq\\Iterator\\SelectIterator' => $vendorDir . '/fusonic/linq/src/Fusonic/Linq/Iterator/SelectIterator.php',
+ 'Fusonic\\Linq\\Iterator\\SelectManyIterator' => $vendorDir . '/fusonic/linq/src/Fusonic/Linq/Iterator/SelectManyIterator.php',
+ 'Fusonic\\Linq\\Iterator\\WhereIterator' => $vendorDir . '/fusonic/linq/src/Fusonic/Linq/Iterator/WhereIterator.php',
+ 'Fusonic\\Linq\\Linq' => $vendorDir . '/fusonic/linq/src/Fusonic/Linq/Linq.php',
+ 'Fusonic\\OpenGraph\\Consumer' => $vendorDir . '/fusonic/opengraph/src/Consumer.php',
+ 'Fusonic\\OpenGraph\\Elements\\Audio' => $vendorDir . '/fusonic/opengraph/src/Elements/Audio.php',
+ 'Fusonic\\OpenGraph\\Elements\\ElementBase' => $vendorDir . '/fusonic/opengraph/src/Elements/ElementBase.php',
+ 'Fusonic\\OpenGraph\\Elements\\Image' => $vendorDir . '/fusonic/opengraph/src/Elements/Image.php',
+ 'Fusonic\\OpenGraph\\Elements\\Video' => $vendorDir . '/fusonic/opengraph/src/Elements/Video.php',
+ 'Fusonic\\OpenGraph\\Objects\\ObjectBase' => $vendorDir . '/fusonic/opengraph/src/Objects/ObjectBase.php',
+ 'Fusonic\\OpenGraph\\Objects\\Website' => $vendorDir . '/fusonic/opengraph/src/Objects/Website.php',
+ 'Fusonic\\OpenGraph\\Property' => $vendorDir . '/fusonic/opengraph/src/Property.php',
+ 'Fusonic\\OpenGraph\\Publisher' => $vendorDir . '/fusonic/opengraph/src/Publisher.php',
'Giggsey\\Locale\\Locale' => $vendorDir . '/giggsey/locale/src/Locale.php',
'GuzzleHttp\\BodySummarizer' => $vendorDir . '/guzzlehttp/guzzle/src/BodySummarizer.php',
'GuzzleHttp\\BodySummarizerInterface' => $vendorDir . '/guzzlehttp/guzzle/src/BodySummarizerInterface.php',
@@ -2864,6 +2885,66 @@ return array(
'Symfony\\Component\\Console\\Tester\\ApplicationTester' => $vendorDir . '/symfony/console/Tester/ApplicationTester.php',
'Symfony\\Component\\Console\\Tester\\CommandTester' => $vendorDir . '/symfony/console/Tester/CommandTester.php',
'Symfony\\Component\\Console\\Tester\\TesterTrait' => $vendorDir . '/symfony/console/Tester/TesterTrait.php',
+ 'Symfony\\Component\\CssSelector\\CssSelectorConverter' => $vendorDir . '/symfony/css-selector/CssSelectorConverter.php',
+ 'Symfony\\Component\\CssSelector\\Exception\\ExceptionInterface' => $vendorDir . '/symfony/css-selector/Exception/ExceptionInterface.php',
+ 'Symfony\\Component\\CssSelector\\Exception\\ExpressionErrorException' => $vendorDir . '/symfony/css-selector/Exception/ExpressionErrorException.php',
+ 'Symfony\\Component\\CssSelector\\Exception\\InternalErrorException' => $vendorDir . '/symfony/css-selector/Exception/InternalErrorException.php',
+ 'Symfony\\Component\\CssSelector\\Exception\\ParseException' => $vendorDir . '/symfony/css-selector/Exception/ParseException.php',
+ 'Symfony\\Component\\CssSelector\\Exception\\SyntaxErrorException' => $vendorDir . '/symfony/css-selector/Exception/SyntaxErrorException.php',
+ 'Symfony\\Component\\CssSelector\\Node\\AbstractNode' => $vendorDir . '/symfony/css-selector/Node/AbstractNode.php',
+ 'Symfony\\Component\\CssSelector\\Node\\AttributeNode' => $vendorDir . '/symfony/css-selector/Node/AttributeNode.php',
+ 'Symfony\\Component\\CssSelector\\Node\\ClassNode' => $vendorDir . '/symfony/css-selector/Node/ClassNode.php',
+ 'Symfony\\Component\\CssSelector\\Node\\CombinedSelectorNode' => $vendorDir . '/symfony/css-selector/Node/CombinedSelectorNode.php',
+ 'Symfony\\Component\\CssSelector\\Node\\ElementNode' => $vendorDir . '/symfony/css-selector/Node/ElementNode.php',
+ 'Symfony\\Component\\CssSelector\\Node\\FunctionNode' => $vendorDir . '/symfony/css-selector/Node/FunctionNode.php',
+ 'Symfony\\Component\\CssSelector\\Node\\HashNode' => $vendorDir . '/symfony/css-selector/Node/HashNode.php',
+ 'Symfony\\Component\\CssSelector\\Node\\NegationNode' => $vendorDir . '/symfony/css-selector/Node/NegationNode.php',
+ 'Symfony\\Component\\CssSelector\\Node\\NodeInterface' => $vendorDir . '/symfony/css-selector/Node/NodeInterface.php',
+ 'Symfony\\Component\\CssSelector\\Node\\PseudoNode' => $vendorDir . '/symfony/css-selector/Node/PseudoNode.php',
+ 'Symfony\\Component\\CssSelector\\Node\\SelectorNode' => $vendorDir . '/symfony/css-selector/Node/SelectorNode.php',
+ 'Symfony\\Component\\CssSelector\\Node\\Specificity' => $vendorDir . '/symfony/css-selector/Node/Specificity.php',
+ 'Symfony\\Component\\CssSelector\\Parser\\Handler\\CommentHandler' => $vendorDir . '/symfony/css-selector/Parser/Handler/CommentHandler.php',
+ 'Symfony\\Component\\CssSelector\\Parser\\Handler\\HandlerInterface' => $vendorDir . '/symfony/css-selector/Parser/Handler/HandlerInterface.php',
+ 'Symfony\\Component\\CssSelector\\Parser\\Handler\\HashHandler' => $vendorDir . '/symfony/css-selector/Parser/Handler/HashHandler.php',
+ 'Symfony\\Component\\CssSelector\\Parser\\Handler\\IdentifierHandler' => $vendorDir . '/symfony/css-selector/Parser/Handler/IdentifierHandler.php',
+ 'Symfony\\Component\\CssSelector\\Parser\\Handler\\NumberHandler' => $vendorDir . '/symfony/css-selector/Parser/Handler/NumberHandler.php',
+ 'Symfony\\Component\\CssSelector\\Parser\\Handler\\StringHandler' => $vendorDir . '/symfony/css-selector/Parser/Handler/StringHandler.php',
+ 'Symfony\\Component\\CssSelector\\Parser\\Handler\\WhitespaceHandler' => $vendorDir . '/symfony/css-selector/Parser/Handler/WhitespaceHandler.php',
+ 'Symfony\\Component\\CssSelector\\Parser\\Parser' => $vendorDir . '/symfony/css-selector/Parser/Parser.php',
+ 'Symfony\\Component\\CssSelector\\Parser\\ParserInterface' => $vendorDir . '/symfony/css-selector/Parser/ParserInterface.php',
+ 'Symfony\\Component\\CssSelector\\Parser\\Reader' => $vendorDir . '/symfony/css-selector/Parser/Reader.php',
+ 'Symfony\\Component\\CssSelector\\Parser\\Shortcut\\ClassParser' => $vendorDir . '/symfony/css-selector/Parser/Shortcut/ClassParser.php',
+ 'Symfony\\Component\\CssSelector\\Parser\\Shortcut\\ElementParser' => $vendorDir . '/symfony/css-selector/Parser/Shortcut/ElementParser.php',
+ 'Symfony\\Component\\CssSelector\\Parser\\Shortcut\\EmptyStringParser' => $vendorDir . '/symfony/css-selector/Parser/Shortcut/EmptyStringParser.php',
+ 'Symfony\\Component\\CssSelector\\Parser\\Shortcut\\HashParser' => $vendorDir . '/symfony/css-selector/Parser/Shortcut/HashParser.php',
+ 'Symfony\\Component\\CssSelector\\Parser\\Token' => $vendorDir . '/symfony/css-selector/Parser/Token.php',
+ 'Symfony\\Component\\CssSelector\\Parser\\TokenStream' => $vendorDir . '/symfony/css-selector/Parser/TokenStream.php',
+ 'Symfony\\Component\\CssSelector\\Parser\\Tokenizer\\Tokenizer' => $vendorDir . '/symfony/css-selector/Parser/Tokenizer/Tokenizer.php',
+ 'Symfony\\Component\\CssSelector\\Parser\\Tokenizer\\TokenizerEscaping' => $vendorDir . '/symfony/css-selector/Parser/Tokenizer/TokenizerEscaping.php',
+ 'Symfony\\Component\\CssSelector\\Parser\\Tokenizer\\TokenizerPatterns' => $vendorDir . '/symfony/css-selector/Parser/Tokenizer/TokenizerPatterns.php',
+ 'Symfony\\Component\\CssSelector\\XPath\\Extension\\AbstractExtension' => $vendorDir . '/symfony/css-selector/XPath/Extension/AbstractExtension.php',
+ 'Symfony\\Component\\CssSelector\\XPath\\Extension\\AttributeMatchingExtension' => $vendorDir . '/symfony/css-selector/XPath/Extension/AttributeMatchingExtension.php',
+ 'Symfony\\Component\\CssSelector\\XPath\\Extension\\CombinationExtension' => $vendorDir . '/symfony/css-selector/XPath/Extension/CombinationExtension.php',
+ 'Symfony\\Component\\CssSelector\\XPath\\Extension\\ExtensionInterface' => $vendorDir . '/symfony/css-selector/XPath/Extension/ExtensionInterface.php',
+ 'Symfony\\Component\\CssSelector\\XPath\\Extension\\FunctionExtension' => $vendorDir . '/symfony/css-selector/XPath/Extension/FunctionExtension.php',
+ 'Symfony\\Component\\CssSelector\\XPath\\Extension\\HtmlExtension' => $vendorDir . '/symfony/css-selector/XPath/Extension/HtmlExtension.php',
+ 'Symfony\\Component\\CssSelector\\XPath\\Extension\\NodeExtension' => $vendorDir . '/symfony/css-selector/XPath/Extension/NodeExtension.php',
+ 'Symfony\\Component\\CssSelector\\XPath\\Extension\\PseudoClassExtension' => $vendorDir . '/symfony/css-selector/XPath/Extension/PseudoClassExtension.php',
+ 'Symfony\\Component\\CssSelector\\XPath\\Translator' => $vendorDir . '/symfony/css-selector/XPath/Translator.php',
+ 'Symfony\\Component\\CssSelector\\XPath\\TranslatorInterface' => $vendorDir . '/symfony/css-selector/XPath/TranslatorInterface.php',
+ 'Symfony\\Component\\CssSelector\\XPath\\XPathExpr' => $vendorDir . '/symfony/css-selector/XPath/XPathExpr.php',
+ 'Symfony\\Component\\DomCrawler\\AbstractUriElement' => $vendorDir . '/symfony/dom-crawler/AbstractUriElement.php',
+ 'Symfony\\Component\\DomCrawler\\Crawler' => $vendorDir . '/symfony/dom-crawler/Crawler.php',
+ 'Symfony\\Component\\DomCrawler\\Field\\ChoiceFormField' => $vendorDir . '/symfony/dom-crawler/Field/ChoiceFormField.php',
+ 'Symfony\\Component\\DomCrawler\\Field\\FileFormField' => $vendorDir . '/symfony/dom-crawler/Field/FileFormField.php',
+ 'Symfony\\Component\\DomCrawler\\Field\\FormField' => $vendorDir . '/symfony/dom-crawler/Field/FormField.php',
+ 'Symfony\\Component\\DomCrawler\\Field\\InputFormField' => $vendorDir . '/symfony/dom-crawler/Field/InputFormField.php',
+ 'Symfony\\Component\\DomCrawler\\Field\\TextareaFormField' => $vendorDir . '/symfony/dom-crawler/Field/TextareaFormField.php',
+ 'Symfony\\Component\\DomCrawler\\Form' => $vendorDir . '/symfony/dom-crawler/Form.php',
+ 'Symfony\\Component\\DomCrawler\\FormFieldRegistry' => $vendorDir . '/symfony/dom-crawler/FormFieldRegistry.php',
+ 'Symfony\\Component\\DomCrawler\\Image' => $vendorDir . '/symfony/dom-crawler/Image.php',
+ 'Symfony\\Component\\DomCrawler\\Link' => $vendorDir . '/symfony/dom-crawler/Link.php',
+ 'Symfony\\Component\\DomCrawler\\UriResolver' => $vendorDir . '/symfony/dom-crawler/UriResolver.php',
'Symfony\\Component\\EventDispatcher\\Debug\\TraceableEventDispatcher' => $vendorDir . '/symfony/event-dispatcher/Debug/TraceableEventDispatcher.php',
'Symfony\\Component\\EventDispatcher\\Debug\\TraceableEventDispatcherInterface' => $vendorDir . '/symfony/event-dispatcher/Debug/TraceableEventDispatcherInterface.php',
'Symfony\\Component\\EventDispatcher\\Debug\\WrappedListener' => $vendorDir . '/symfony/event-dispatcher/Debug/WrappedListener.php',
diff --git a/composer/autoload_files.php b/composer/autoload_files.php
index d6a3e264..0b30e898 100644
--- a/composer/autoload_files.php
+++ b/composer/autoload_files.php
@@ -6,13 +6,13 @@ $vendorDir = dirname(__DIR__);
$baseDir = $vendorDir;
return array(
- '383eaff206634a77a1be54e64e6459c7' => $vendorDir . '/sabre/uri/lib/functions.php',
'a4a119a56e50fbb293281d9a48007e0e' => $vendorDir . '/symfony/polyfill-php80/bootstrap.php',
- '7b11c4dc42b3b3023073cb14e519683c' => $vendorDir . '/ralouphie/getallheaders/src/getallheaders.php',
+ '383eaff206634a77a1be54e64e6459c7' => $vendorDir . '/sabre/uri/lib/functions.php',
'0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => $vendorDir . '/symfony/polyfill-mbstring/bootstrap.php',
+ '6e3fae29631ef280660b3cdad06f25a8' => $vendorDir . '/symfony/deprecation-contracts/function.php',
+ '7b11c4dc42b3b3023073cb14e519683c' => $vendorDir . '/ralouphie/getallheaders/src/getallheaders.php',
'c964ee0ededf28c96ebd9db5099ef910' => $vendorDir . '/guzzlehttp/promises/src/functions_include.php',
'a0edc8309cc5e1d60e3047b5df6b7052' => $vendorDir . '/guzzlehttp/psr7/src/functions_include.php',
- '6e3fae29631ef280660b3cdad06f25a8' => $vendorDir . '/symfony/deprecation-contracts/function.php',
'a4ecaeafb8cfb009ad0e052c90355e98' => $vendorDir . '/beberlei/assert/lib/Assert/functions.php',
'37a3dc5111fe8f707ab4c132ef1dbc62' => $vendorDir . '/guzzlehttp/guzzle/src/functions_include.php',
'2b9d0f43f9552984cfa82fee95491826' => $vendorDir . '/sabre/event/lib/coroutine.php',
@@ -20,11 +20,11 @@ return array(
'a1cce3d26cc15c00fcd0b3354bd72c88' => $vendorDir . '/sabre/event/lib/Promise/functions.php',
'3569eecfeed3bcf0bad3c998a494ecb8' => $vendorDir . '/sabre/xml/lib/Deserializer/functions.php',
'93aa591bc4ca510c520999e34229ee79' => $vendorDir . '/sabre/xml/lib/Serializer/functions.php',
+ '320cde22f66dd4f5d3fd621d3e88b98f' => $vendorDir . '/symfony/polyfill-ctype/bootstrap.php',
'e69f7f6ee287b969198c3c9d6777bd38' => $vendorDir . '/symfony/polyfill-intl-normalizer/bootstrap.php',
'25072dd6e2470089de65ae7bf11d3109' => $vendorDir . '/symfony/polyfill-php72/bootstrap.php',
'f598d06aa772fa33d905e87be6398fb1' => $vendorDir . '/symfony/polyfill-intl-idn/bootstrap.php',
'ebdb698ed4152ae445614b69b5e4bb6a' => $vendorDir . '/sabre/http/lib/functions.php',
- '320cde22f66dd4f5d3fd621d3e88b98f' => $vendorDir . '/symfony/polyfill-ctype/bootstrap.php',
'0d59ee240a4cd96ddbb4ff164fccea4d' => $vendorDir . '/symfony/polyfill-php73/bootstrap.php',
'b067bc7112e384b61c701452d53a14a8' => $vendorDir . '/mtdowling/jmespath.php/src/JmesPath.php',
'e39a8b23c42d4e1452234d762b03835a' => $vendorDir . '/ramsey/uuid/src/functions.php',
diff --git a/composer/autoload_namespaces.php b/composer/autoload_namespaces.php
index 225f7eb6..c43ac488 100644
--- a/composer/autoload_namespaces.php
+++ b/composer/autoload_namespaces.php
@@ -7,6 +7,7 @@ $baseDir = $vendorDir;
return array(
'Pimple' => array($vendorDir . '/pimple/pimple/src'),
+ 'Fusonic\\Linq' => array($vendorDir . '/fusonic/linq/src'),
'Console' => array($vendorDir . '/pear/console_getopt'),
'Archive_Tar' => array($vendorDir . '/pear/archive_tar'),
'' => array($vendorDir . '/pear/pear-core-minimal/src'),
diff --git a/composer/autoload_psr4.php b/composer/autoload_psr4.php
index e7b84631..4b572bd1 100644
--- a/composer/autoload_psr4.php
+++ b/composer/autoload_psr4.php
@@ -31,6 +31,8 @@ return array(
'Symfony\\Component\\Process\\' => array($vendorDir . '/symfony/process'),
'Symfony\\Component\\HttpFoundation\\' => array($vendorDir . '/symfony/http-foundation'),
'Symfony\\Component\\EventDispatcher\\' => array($vendorDir . '/symfony/event-dispatcher'),
+ 'Symfony\\Component\\DomCrawler\\' => array($vendorDir . '/symfony/dom-crawler'),
+ 'Symfony\\Component\\CssSelector\\' => array($vendorDir . '/symfony/css-selector'),
'Symfony\\Component\\Console\\' => array($vendorDir . '/symfony/console'),
'Stecman\\Component\\Symfony\\Console\\BashCompletion\\' => array($vendorDir . '/stecman/symfony-console-completion/src'),
'SearchDAV\\' => array($vendorDir . '/icewind/searchdav/src'),
@@ -74,6 +76,7 @@ return array(
'GuzzleHttp\\Promise\\' => array($vendorDir . '/guzzlehttp/promises/src'),
'GuzzleHttp\\' => array($vendorDir . '/guzzlehttp/guzzle/src'),
'Giggsey\\Locale\\' => array($vendorDir . '/giggsey/locale/src'),
+ 'Fusonic\\OpenGraph\\' => array($vendorDir . '/fusonic/opengraph/src'),
'FG\\' => array($vendorDir . '/fgrosse/phpasn1/lib'),
'Egulias\\EmailValidator\\' => array($vendorDir . '/egulias/email-validator/src'),
'Ds\\' => array($vendorDir . '/php-ds/php-ds/src'),
diff --git a/composer/autoload_static.php b/composer/autoload_static.php
index a195c71d..7efc962f 100644
--- a/composer/autoload_static.php
+++ b/composer/autoload_static.php
@@ -7,13 +7,13 @@ namespace Composer\Autoload;
class ComposerStaticInit2f23f73bc0cc116b4b1eee1521aa8652
{
public static $files = array (
- '383eaff206634a77a1be54e64e6459c7' => __DIR__ . '/..' . '/sabre/uri/lib/functions.php',
'a4a119a56e50fbb293281d9a48007e0e' => __DIR__ . '/..' . '/symfony/polyfill-php80/bootstrap.php',
- '7b11c4dc42b3b3023073cb14e519683c' => __DIR__ . '/..' . '/ralouphie/getallheaders/src/getallheaders.php',
+ '383eaff206634a77a1be54e64e6459c7' => __DIR__ . '/..' . '/sabre/uri/lib/functions.php',
'0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => __DIR__ . '/..' . '/symfony/polyfill-mbstring/bootstrap.php',
+ '6e3fae29631ef280660b3cdad06f25a8' => __DIR__ . '/..' . '/symfony/deprecation-contracts/function.php',
+ '7b11c4dc42b3b3023073cb14e519683c' => __DIR__ . '/..' . '/ralouphie/getallheaders/src/getallheaders.php',
'c964ee0ededf28c96ebd9db5099ef910' => __DIR__ . '/..' . '/guzzlehttp/promises/src/functions_include.php',
'a0edc8309cc5e1d60e3047b5df6b7052' => __DIR__ . '/..' . '/guzzlehttp/psr7/src/functions_include.php',
- '6e3fae29631ef280660b3cdad06f25a8' => __DIR__ . '/..' . '/symfony/deprecation-contracts/function.php',
'a4ecaeafb8cfb009ad0e052c90355e98' => __DIR__ . '/..' . '/beberlei/assert/lib/Assert/functions.php',
'37a3dc5111fe8f707ab4c132ef1dbc62' => __DIR__ . '/..' . '/guzzlehttp/guzzle/src/functions_include.php',
'2b9d0f43f9552984cfa82fee95491826' => __DIR__ . '/..' . '/sabre/event/lib/coroutine.php',
@@ -21,11 +21,11 @@ class ComposerStaticInit2f23f73bc0cc116b4b1eee1521aa8652
'a1cce3d26cc15c00fcd0b3354bd72c88' => __DIR__ . '/..' . '/sabre/event/lib/Promise/functions.php',
'3569eecfeed3bcf0bad3c998a494ecb8' => __DIR__ . '/..' . '/sabre/xml/lib/Deserializer/functions.php',
'93aa591bc4ca510c520999e34229ee79' => __DIR__ . '/..' . '/sabre/xml/lib/Serializer/functions.php',
+ '320cde22f66dd4f5d3fd621d3e88b98f' => __DIR__ . '/..' . '/symfony/polyfill-ctype/bootstrap.php',
'e69f7f6ee287b969198c3c9d6777bd38' => __DIR__ . '/..' . '/symfony/polyfill-intl-normalizer/bootstrap.php',
'25072dd6e2470089de65ae7bf11d3109' => __DIR__ . '/..' . '/symfony/polyfill-php72/bootstrap.php',
'f598d06aa772fa33d905e87be6398fb1' => __DIR__ . '/..' . '/symfony/polyfill-intl-idn/bootstrap.php',
'ebdb698ed4152ae445614b69b5e4bb6a' => __DIR__ . '/..' . '/sabre/http/lib/functions.php',
- '320cde22f66dd4f5d3fd621d3e88b98f' => __DIR__ . '/..' . '/symfony/polyfill-ctype/bootstrap.php',
'0d59ee240a4cd96ddbb4ff164fccea4d' => __DIR__ . '/..' . '/symfony/polyfill-php73/bootstrap.php',
'b067bc7112e384b61c701452d53a14a8' => __DIR__ . '/..' . '/mtdowling/jmespath.php/src/JmesPath.php',
'e39a8b23c42d4e1452234d762b03835a' => __DIR__ . '/..' . '/ramsey/uuid/src/functions.php',
@@ -175,6 +175,8 @@ class ComposerStaticInit2f23f73bc0cc116b4b1eee1521aa8652
'Symfony\\Component\\Process\\' => 26,
'Symfony\\Component\\HttpFoundation\\' => 33,
'Symfony\\Component\\EventDispatcher\\' => 34,
+ 'Symfony\\Component\\DomCrawler\\' => 29,
+ 'Symfony\\Component\\CssSelector\\' => 30,
'Symfony\\Component\\Console\\' => 26,
'Stecman\\Component\\Symfony\\Console\\BashCompletion\\' => 49,
'SearchDAV\\' => 10,
@@ -251,6 +253,7 @@ class ComposerStaticInit2f23f73bc0cc116b4b1eee1521aa8652
),
'F' =>
array (
+ 'Fusonic\\OpenGraph\\' => 18,
'FG\\' => 3,
),
'E' =>
@@ -384,6 +387,14 @@ class ComposerStaticInit2f23f73bc0cc116b4b1eee1521aa8652
array (
0 => __DIR__ . '/..' . '/symfony/event-dispatcher',
),
+ 'Symfony\\Component\\DomCrawler\\' =>
+ array (
+ 0 => __DIR__ . '/..' . '/symfony/dom-crawler',
+ ),
+ 'Symfony\\Component\\CssSelector\\' =>
+ array (
+ 0 => __DIR__ . '/..' . '/symfony/css-selector',
+ ),
'Symfony\\Component\\Console\\' =>
array (
0 => __DIR__ . '/..' . '/symfony/console',
@@ -560,6 +571,10 @@ class ComposerStaticInit2f23f73bc0cc116b4b1eee1521aa8652
array (
0 => __DIR__ . '/..' . '/giggsey/locale/src',
),
+ 'Fusonic\\OpenGraph\\' =>
+ array (
+ 0 => __DIR__ . '/..' . '/fusonic/opengraph/src',
+ ),
'FG\\' =>
array (
0 => __DIR__ . '/..' . '/fgrosse/phpasn1/lib',
@@ -626,6 +641,13 @@ class ComposerStaticInit2f23f73bc0cc116b4b1eee1521aa8652
0 => __DIR__ . '/..' . '/pimple/pimple/src',
),
),
+ 'F' =>
+ array (
+ 'Fusonic\\Linq' =>
+ array (
+ 0 => __DIR__ . '/..' . '/fusonic/linq/src',
+ ),
+ ),
'C' =>
array (
'Console' =>
@@ -1974,6 +1996,27 @@ class ComposerStaticInit2f23f73bc0cc116b4b1eee1521aa8652
'FG\\X509\\SAN\\DNSName' => __DIR__ . '/..' . '/fgrosse/phpasn1/lib/X509/SAN/DNSName.php',
'FG\\X509\\SAN\\IPAddress' => __DIR__ . '/..' . '/fgrosse/phpasn1/lib/X509/SAN/IPAddress.php',
'FG\\X509\\SAN\\SubjectAlternativeNames' => __DIR__ . '/..' . '/fgrosse/phpasn1/lib/X509/SAN/SubjectAlternativeNames.php',
+ 'Fusonic\\Linq\\GroupedLinq' => __DIR__ . '/..' . '/fusonic/linq/src/Fusonic/Linq/GroupedLinq.php',
+ 'Fusonic\\Linq\\Helper\\LinqHelper' => __DIR__ . '/..' . '/fusonic/linq/src/Fusonic/Linq/Helper/LinqHelper.php',
+ 'Fusonic\\Linq\\Iterator\\DistinctIterator' => __DIR__ . '/..' . '/fusonic/linq/src/Fusonic/Linq/Iterator/DistinctIterator.php',
+ 'Fusonic\\Linq\\Iterator\\ExceptIterator' => __DIR__ . '/..' . '/fusonic/linq/src/Fusonic/Linq/Iterator/ExceptIterator.php',
+ 'Fusonic\\Linq\\Iterator\\GroupIterator' => __DIR__ . '/..' . '/fusonic/linq/src/Fusonic/Linq/Iterator/GroupIterator.php',
+ 'Fusonic\\Linq\\Iterator\\IntersectIterator' => __DIR__ . '/..' . '/fusonic/linq/src/Fusonic/Linq/Iterator/IntersectIterator.php',
+ 'Fusonic\\Linq\\Iterator\\OfTypeIterator' => __DIR__ . '/..' . '/fusonic/linq/src/Fusonic/Linq/Iterator/OfTypeIterator.php',
+ 'Fusonic\\Linq\\Iterator\\OrderIterator' => __DIR__ . '/..' . '/fusonic/linq/src/Fusonic/Linq/Iterator/OrderIterator.php',
+ 'Fusonic\\Linq\\Iterator\\SelectIterator' => __DIR__ . '/..' . '/fusonic/linq/src/Fusonic/Linq/Iterator/SelectIterator.php',
+ 'Fusonic\\Linq\\Iterator\\SelectManyIterator' => __DIR__ . '/..' . '/fusonic/linq/src/Fusonic/Linq/Iterator/SelectManyIterator.php',
+ 'Fusonic\\Linq\\Iterator\\WhereIterator' => __DIR__ . '/..' . '/fusonic/linq/src/Fusonic/Linq/Iterator/WhereIterator.php',
+ 'Fusonic\\Linq\\Linq' => __DIR__ . '/..' . '/fusonic/linq/src/Fusonic/Linq/Linq.php',
+ 'Fusonic\\OpenGraph\\Consumer' => __DIR__ . '/..' . '/fusonic/opengraph/src/Consumer.php',
+ 'Fusonic\\OpenGraph\\Elements\\Audio' => __DIR__ . '/..' . '/fusonic/opengraph/src/Elements/Audio.php',
+ 'Fusonic\\OpenGraph\\Elements\\ElementBase' => __DIR__ . '/..' . '/fusonic/opengraph/src/Elements/ElementBase.php',
+ 'Fusonic\\OpenGraph\\Elements\\Image' => __DIR__ . '/..' . '/fusonic/opengraph/src/Elements/Image.php',
+ 'Fusonic\\OpenGraph\\Elements\\Video' => __DIR__ . '/..' . '/fusonic/opengraph/src/Elements/Video.php',
+ 'Fusonic\\OpenGraph\\Objects\\ObjectBase' => __DIR__ . '/..' . '/fusonic/opengraph/src/Objects/ObjectBase.php',
+ 'Fusonic\\OpenGraph\\Objects\\Website' => __DIR__ . '/..' . '/fusonic/opengraph/src/Objects/Website.php',
+ 'Fusonic\\OpenGraph\\Property' => __DIR__ . '/..' . '/fusonic/opengraph/src/Property.php',
+ 'Fusonic\\OpenGraph\\Publisher' => __DIR__ . '/..' . '/fusonic/opengraph/src/Publisher.php',
'Giggsey\\Locale\\Locale' => __DIR__ . '/..' . '/giggsey/locale/src/Locale.php',
'GuzzleHttp\\BodySummarizer' => __DIR__ . '/..' . '/guzzlehttp/guzzle/src/BodySummarizer.php',
'GuzzleHttp\\BodySummarizerInterface' => __DIR__ . '/..' . '/guzzlehttp/guzzle/src/BodySummarizerInterface.php',
@@ -3505,6 +3548,66 @@ class ComposerStaticInit2f23f73bc0cc116b4b1eee1521aa8652
'Symfony\\Component\\Console\\Tester\\ApplicationTester' => __DIR__ . '/..' . '/symfony/console/Tester/ApplicationTester.php',
'Symfony\\Component\\Console\\Tester\\CommandTester' => __DIR__ . '/..' . '/symfony/console/Tester/CommandTester.php',
'Symfony\\Component\\Console\\Tester\\TesterTrait' => __DIR__ . '/..' . '/symfony/console/Tester/TesterTrait.php',
+ 'Symfony\\Component\\CssSelector\\CssSelectorConverter' => __DIR__ . '/..' . '/symfony/css-selector/CssSelectorConverter.php',
+ 'Symfony\\Component\\CssSelector\\Exception\\ExceptionInterface' => __DIR__ . '/..' . '/symfony/css-selector/Exception/ExceptionInterface.php',
+ 'Symfony\\Component\\CssSelector\\Exception\\ExpressionErrorException' => __DIR__ . '/..' . '/symfony/css-selector/Exception/ExpressionErrorException.php',
+ 'Symfony\\Component\\CssSelector\\Exception\\InternalErrorException' => __DIR__ . '/..' . '/symfony/css-selector/Exception/InternalErrorException.php',
+ 'Symfony\\Component\\CssSelector\\Exception\\ParseException' => __DIR__ . '/..' . '/symfony/css-selector/Exception/ParseException.php',
+ 'Symfony\\Component\\CssSelector\\Exception\\SyntaxErrorException' => __DIR__ . '/..' . '/symfony/css-selector/Exception/SyntaxErrorException.php',
+ 'Symfony\\Component\\CssSelector\\Node\\AbstractNode' => __DIR__ . '/..' . '/symfony/css-selector/Node/AbstractNode.php',
+ 'Symfony\\Component\\CssSelector\\Node\\AttributeNode' => __DIR__ . '/..' . '/symfony/css-selector/Node/AttributeNode.php',
+ 'Symfony\\Component\\CssSelector\\Node\\ClassNode' => __DIR__ . '/..' . '/symfony/css-selector/Node/ClassNode.php',
+ 'Symfony\\Component\\CssSelector\\Node\\CombinedSelectorNode' => __DIR__ . '/..' . '/symfony/css-selector/Node/CombinedSelectorNode.php',
+ 'Symfony\\Component\\CssSelector\\Node\\ElementNode' => __DIR__ . '/..' . '/symfony/css-selector/Node/ElementNode.php',
+ 'Symfony\\Component\\CssSelector\\Node\\FunctionNode' => __DIR__ . '/..' . '/symfony/css-selector/Node/FunctionNode.php',
+ 'Symfony\\Component\\CssSelector\\Node\\HashNode' => __DIR__ . '/..' . '/symfony/css-selector/Node/HashNode.php',
+ 'Symfony\\Component\\CssSelector\\Node\\NegationNode' => __DIR__ . '/..' . '/symfony/css-selector/Node/NegationNode.php',
+ 'Symfony\\Component\\CssSelector\\Node\\NodeInterface' => __DIR__ . '/..' . '/symfony/css-selector/Node/NodeInterface.php',
+ 'Symfony\\Component\\CssSelector\\Node\\PseudoNode' => __DIR__ . '/..' . '/symfony/css-selector/Node/PseudoNode.php',
+ 'Symfony\\Component\\CssSelector\\Node\\SelectorNode' => __DIR__ . '/..' . '/symfony/css-selector/Node/SelectorNode.php',
+ 'Symfony\\Component\\CssSelector\\Node\\Specificity' => __DIR__ . '/..' . '/symfony/css-selector/Node/Specificity.php',
+ 'Symfony\\Component\\CssSelector\\Parser\\Handler\\CommentHandler' => __DIR__ . '/..' . '/symfony/css-selector/Parser/Handler/CommentHandler.php',
+ 'Symfony\\Component\\CssSelector\\Parser\\Handler\\HandlerInterface' => __DIR__ . '/..' . '/symfony/css-selector/Parser/Handler/HandlerInterface.php',
+ 'Symfony\\Component\\CssSelector\\Parser\\Handler\\HashHandler' => __DIR__ . '/..' . '/symfony/css-selector/Parser/Handler/HashHandler.php',
+ 'Symfony\\Component\\CssSelector\\Parser\\Handler\\IdentifierHandler' => __DIR__ . '/..' . '/symfony/css-selector/Parser/Handler/IdentifierHandler.php',
+ 'Symfony\\Component\\CssSelector\\Parser\\Handler\\NumberHandler' => __DIR__ . '/..' . '/symfony/css-selector/Parser/Handler/NumberHandler.php',
+ 'Symfony\\Component\\CssSelector\\Parser\\Handler\\StringHandler' => __DIR__ . '/..' . '/symfony/css-selector/Parser/Handler/StringHandler.php',
+ 'Symfony\\Component\\CssSelector\\Parser\\Handler\\WhitespaceHandler' => __DIR__ . '/..' . '/symfony/css-selector/Parser/Handler/WhitespaceHandler.php',
+ 'Symfony\\Component\\CssSelector\\Parser\\Parser' => __DIR__ . '/..' . '/symfony/css-selector/Parser/Parser.php',
+ 'Symfony\\Component\\CssSelector\\Parser\\ParserInterface' => __DIR__ . '/..' . '/symfony/css-selector/Parser/ParserInterface.php',
+ 'Symfony\\Component\\CssSelector\\Parser\\Reader' => __DIR__ . '/..' . '/symfony/css-selector/Parser/Reader.php',
+ 'Symfony\\Component\\CssSelector\\Parser\\Shortcut\\ClassParser' => __DIR__ . '/..' . '/symfony/css-selector/Parser/Shortcut/ClassParser.php',
+ 'Symfony\\Component\\CssSelector\\Parser\\Shortcut\\ElementParser' => __DIR__ . '/..' . '/symfony/css-selector/Parser/Shortcut/ElementParser.php',
+ 'Symfony\\Component\\CssSelector\\Parser\\Shortcut\\EmptyStringParser' => __DIR__ . '/..' . '/symfony/css-selector/Parser/Shortcut/EmptyStringParser.php',
+ 'Symfony\\Component\\CssSelector\\Parser\\Shortcut\\HashParser' => __DIR__ . '/..' . '/symfony/css-selector/Parser/Shortcut/HashParser.php',
+ 'Symfony\\Component\\CssSelector\\Parser\\Token' => __DIR__ . '/..' . '/symfony/css-selector/Parser/Token.php',
+ 'Symfony\\Component\\CssSelector\\Parser\\TokenStream' => __DIR__ . '/..' . '/symfony/css-selector/Parser/TokenStream.php',
+ 'Symfony\\Component\\CssSelector\\Parser\\Tokenizer\\Tokenizer' => __DIR__ . '/..' . '/symfony/css-selector/Parser/Tokenizer/Tokenizer.php',
+ 'Symfony\\Component\\CssSelector\\Parser\\Tokenizer\\TokenizerEscaping' => __DIR__ . '/..' . '/symfony/css-selector/Parser/Tokenizer/TokenizerEscaping.php',
+ 'Symfony\\Component\\CssSelector\\Parser\\Tokenizer\\TokenizerPatterns' => __DIR__ . '/..' . '/symfony/css-selector/Parser/Tokenizer/TokenizerPatterns.php',
+ 'Symfony\\Component\\CssSelector\\XPath\\Extension\\AbstractExtension' => __DIR__ . '/..' . '/symfony/css-selector/XPath/Extension/AbstractExtension.php',
+ 'Symfony\\Component\\CssSelector\\XPath\\Extension\\AttributeMatchingExtension' => __DIR__ . '/..' . '/symfony/css-selector/XPath/Extension/AttributeMatchingExtension.php',
+ 'Symfony\\Component\\CssSelector\\XPath\\Extension\\CombinationExtension' => __DIR__ . '/..' . '/symfony/css-selector/XPath/Extension/CombinationExtension.php',
+ 'Symfony\\Component\\CssSelector\\XPath\\Extension\\ExtensionInterface' => __DIR__ . '/..' . '/symfony/css-selector/XPath/Extension/ExtensionInterface.php',
+ 'Symfony\\Component\\CssSelector\\XPath\\Extension\\FunctionExtension' => __DIR__ . '/..' . '/symfony/css-selector/XPath/Extension/FunctionExtension.php',
+ 'Symfony\\Component\\CssSelector\\XPath\\Extension\\HtmlExtension' => __DIR__ . '/..' . '/symfony/css-selector/XPath/Extension/HtmlExtension.php',
+ 'Symfony\\Component\\CssSelector\\XPath\\Extension\\NodeExtension' => __DIR__ . '/..' . '/symfony/css-selector/XPath/Extension/NodeExtension.php',
+ 'Symfony\\Component\\CssSelector\\XPath\\Extension\\PseudoClassExtension' => __DIR__ . '/..' . '/symfony/css-selector/XPath/Extension/PseudoClassExtension.php',
+ 'Symfony\\Component\\CssSelector\\XPath\\Translator' => __DIR__ . '/..' . '/symfony/css-selector/XPath/Translator.php',
+ 'Symfony\\Component\\CssSelector\\XPath\\TranslatorInterface' => __DIR__ . '/..' . '/symfony/css-selector/XPath/TranslatorInterface.php',
+ 'Symfony\\Component\\CssSelector\\XPath\\XPathExpr' => __DIR__ . '/..' . '/symfony/css-selector/XPath/XPathExpr.php',
+ 'Symfony\\Component\\DomCrawler\\AbstractUriElement' => __DIR__ . '/..' . '/symfony/dom-crawler/AbstractUriElement.php',
+ 'Symfony\\Component\\DomCrawler\\Crawler' => __DIR__ . '/..' . '/symfony/dom-crawler/Crawler.php',
+ 'Symfony\\Component\\DomCrawler\\Field\\ChoiceFormField' => __DIR__ . '/..' . '/symfony/dom-crawler/Field/ChoiceFormField.php',
+ 'Symfony\\Component\\DomCrawler\\Field\\FileFormField' => __DIR__ . '/..' . '/symfony/dom-crawler/Field/FileFormField.php',
+ 'Symfony\\Component\\DomCrawler\\Field\\FormField' => __DIR__ . '/..' . '/symfony/dom-crawler/Field/FormField.php',
+ 'Symfony\\Component\\DomCrawler\\Field\\InputFormField' => __DIR__ . '/..' . '/symfony/dom-crawler/Field/InputFormField.php',
+ 'Symfony\\Component\\DomCrawler\\Field\\TextareaFormField' => __DIR__ . '/..' . '/symfony/dom-crawler/Field/TextareaFormField.php',
+ 'Symfony\\Component\\DomCrawler\\Form' => __DIR__ . '/..' . '/symfony/dom-crawler/Form.php',
+ 'Symfony\\Component\\DomCrawler\\FormFieldRegistry' => __DIR__ . '/..' . '/symfony/dom-crawler/FormFieldRegistry.php',
+ 'Symfony\\Component\\DomCrawler\\Image' => __DIR__ . '/..' . '/symfony/dom-crawler/Image.php',
+ 'Symfony\\Component\\DomCrawler\\Link' => __DIR__ . '/..' . '/symfony/dom-crawler/Link.php',
+ 'Symfony\\Component\\DomCrawler\\UriResolver' => __DIR__ . '/..' . '/symfony/dom-crawler/UriResolver.php',
'Symfony\\Component\\EventDispatcher\\Debug\\TraceableEventDispatcher' => __DIR__ . '/..' . '/symfony/event-dispatcher/Debug/TraceableEventDispatcher.php',
'Symfony\\Component\\EventDispatcher\\Debug\\TraceableEventDispatcherInterface' => __DIR__ . '/..' . '/symfony/event-dispatcher/Debug/TraceableEventDispatcherInterface.php',
'Symfony\\Component\\EventDispatcher\\Debug\\WrappedListener' => __DIR__ . '/..' . '/symfony/event-dispatcher/Debug/WrappedListener.php',
diff --git a/composer/installed.json b/composer/installed.json
index 95b1b4c8..1cb2e013 100644
--- a/composer/installed.json
+++ b/composer/installed.json
@@ -1132,6 +1132,116 @@
"install-path": "../fgrosse/phpasn1"
},
{
+ "name": "fusonic/linq",
+ "version": "v1.1.0",
+ "version_normalized": "1.1.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/fusonic/linq.git",
+ "reference": "63520ef1470ca771acbd26871efb945dd4a7a5d8"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/fusonic/linq/zipball/63520ef1470ca771acbd26871efb945dd4a7a5d8",
+ "reference": "63520ef1470ca771acbd26871efb945dd4a7a5d8",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.2"
+ },
+ "time": "2015-02-26T22:49:17+00:00",
+ "type": "library",
+ "installation-source": "dist",
+ "autoload": {
+ "psr-0": {
+ "Fusonic\\Linq": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fusonic",
+ "homepage": "http://www.fusonic.net"
+ }
+ ],
+ "description": "LINQ 2 objects class for PHP",
+ "homepage": "http://fusonic.github.io/fusonic-linq/",
+ "keywords": [
+ "linq",
+ "linq2objects"
+ ],
+ "support": {
+ "issues": "https://github.com/fusonic/linq/issues",
+ "source": "https://github.com/fusonic/linq/tree/master"
+ },
+ "install-path": "../fusonic/linq"
+ },
+ {
+ "name": "fusonic/opengraph",
+ "version": "v2.2.0",
+ "version_normalized": "2.2.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/fusonic/opengraph.git",
+ "reference": "a63b588fbe56c175ae06e158f1513642653ee3c1"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/fusonic/opengraph/zipball/a63b588fbe56c175ae06e158f1513642653ee3c1",
+ "reference": "a63b588fbe56c175ae06e158f1513642653ee3c1",
+ "shasum": ""
+ },
+ "require": {
+ "ext-dom": "*",
+ "fusonic/linq": "^1.0",
+ "php": "^7.4|^8.0",
+ "psr/http-client": "^1.0",
+ "psr/http-factory": "^1.0",
+ "symfony/css-selector": "^3.0|^4.0|^5.0|^6.0",
+ "symfony/dom-crawler": "^3.0|^4.0|^5.0|^6.0"
+ },
+ "require-dev": {
+ "nyholm/psr7": "^1.2",
+ "phpunit/phpunit": "^9.0",
+ "symfony/http-client": "^6.0"
+ },
+ "suggest": {
+ "nyholm/psr7": "^1.2",
+ "symfony/http-client": "^5.0"
+ },
+ "time": "2022-01-20T05:47:36+00:00",
+ "type": "library",
+ "installation-source": "dist",
+ "autoload": {
+ "psr-4": {
+ "Fusonic\\OpenGraph\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fusonic",
+ "homepage": "https://www.fusonic.net"
+ }
+ ],
+ "description": "PHP library for consuming and publishing Open Graph resources.",
+ "homepage": "https://github.com/fusonic/fusonic-opengraph",
+ "keywords": [
+ "opengraph"
+ ],
+ "support": {
+ "issues": "https://github.com/fusonic/opengraph/issues",
+ "source": "https://github.com/fusonic/opengraph/tree/v2.2.0"
+ },
+ "install-path": "../fusonic/opengraph"
+ },
+ {
"name": "giggsey/libphonenumber-for-php",
"version": "8.12.38",
"version_normalized": "8.12.38.0",
@@ -4718,6 +4828,75 @@
"install-path": "../symfony/console"
},
{
+ "name": "symfony/css-selector",
+ "version": "v5.4.11",
+ "version_normalized": "5.4.11.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/css-selector.git",
+ "reference": "c1681789f059ab756001052164726ae88512ae3d"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/css-selector/zipball/c1681789f059ab756001052164726ae88512ae3d",
+ "reference": "c1681789f059ab756001052164726ae88512ae3d",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2.5",
+ "symfony/polyfill-php80": "^1.16"
+ },
+ "time": "2022-06-27T16:58:25+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/v5.4.11"
+ },
+ "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/deprecation-contracts",
"version": "v2.5.2",
"version_normalized": "2.5.2.0",
@@ -4788,6 +4967,84 @@
"install-path": "../symfony/deprecation-contracts"
},
{
+ "name": "symfony/dom-crawler",
+ "version": "v5.4.11",
+ "version_normalized": "5.4.11.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/dom-crawler.git",
+ "reference": "0b900ca5576ecd59e08c76127e616667cfe427a7"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/0b900ca5576ecd59e08c76127e616667cfe427a7",
+ "reference": "0b900ca5576ecd59e08c76127e616667cfe427a7",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2.5",
+ "symfony/deprecation-contracts": "^2.1|^3",
+ "symfony/polyfill-ctype": "~1.8",
+ "symfony/polyfill-mbstring": "~1.0",
+ "symfony/polyfill-php80": "^1.16"
+ },
+ "conflict": {
+ "masterminds/html5": "<2.6"
+ },
+ "require-dev": {
+ "masterminds/html5": "^2.6",
+ "symfony/css-selector": "^4.4|^5.0|^6.0"
+ },
+ "suggest": {
+ "symfony/css-selector": ""
+ },
+ "time": "2022-06-27T16:58:25+00:00",
+ "type": "library",
+ "installation-source": "dist",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\DomCrawler\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Eases DOM navigation for HTML and XML documents",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/dom-crawler/tree/v5.4.11"
+ },
+ "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/dom-crawler"
+ },
+ {
"name": "symfony/event-dispatcher",
"version": "v4.4.30",
"version_normalized": "4.4.30.0",
diff --git a/composer/installed.php b/composer/installed.php
index 58c14fb1..832b77ba 100644
--- a/composer/installed.php
+++ b/composer/installed.php
@@ -3,7 +3,7 @@
'name' => 'nextcloud/3rdparty',
'pretty_version' => 'dev-master',
'version' => 'dev-master',
- 'reference' => '2d3ffebfcfb22af6d999d575b53e8f41cdfb4e9f',
+ 'reference' => '143faae0eba0121682129d1261158dd9db74b2b2',
'type' => 'library',
'install_path' => __DIR__ . '/../',
'aliases' => array(),
@@ -154,6 +154,24 @@
'aliases' => array(),
'dev_requirement' => false,
),
+ 'fusonic/linq' => array(
+ 'pretty_version' => 'v1.1.0',
+ 'version' => '1.1.0.0',
+ 'reference' => '63520ef1470ca771acbd26871efb945dd4a7a5d8',
+ 'type' => 'library',
+ 'install_path' => __DIR__ . '/../fusonic/linq',
+ 'aliases' => array(),
+ 'dev_requirement' => false,
+ ),
+ 'fusonic/opengraph' => array(
+ 'pretty_version' => 'v2.2.0',
+ 'version' => '2.2.0.0',
+ 'reference' => 'a63b588fbe56c175ae06e158f1513642653ee3c1',
+ 'type' => 'library',
+ 'install_path' => __DIR__ . '/../fusonic/opengraph',
+ 'aliases' => array(),
+ 'dev_requirement' => false,
+ ),
'giggsey/libphonenumber-for-php' => array(
'pretty_version' => '8.12.38',
'version' => '8.12.38.0',
@@ -301,7 +319,7 @@
'nextcloud/3rdparty' => array(
'pretty_version' => 'dev-master',
'version' => 'dev-master',
- 'reference' => '2d3ffebfcfb22af6d999d575b53e8f41cdfb4e9f',
+ 'reference' => '143faae0eba0121682129d1261158dd9db74b2b2',
'type' => 'library',
'install_path' => __DIR__ . '/../',
'aliases' => array(),
@@ -697,6 +715,15 @@
'aliases' => array(),
'dev_requirement' => false,
),
+ 'symfony/css-selector' => array(
+ 'pretty_version' => 'v5.4.11',
+ 'version' => '5.4.11.0',
+ 'reference' => 'c1681789f059ab756001052164726ae88512ae3d',
+ 'type' => 'library',
+ 'install_path' => __DIR__ . '/../symfony/css-selector',
+ 'aliases' => array(),
+ 'dev_requirement' => false,
+ ),
'symfony/deprecation-contracts' => array(
'pretty_version' => 'v2.5.2',
'version' => '2.5.2.0',
@@ -706,6 +733,15 @@
'aliases' => array(),
'dev_requirement' => false,
),
+ 'symfony/dom-crawler' => array(
+ 'pretty_version' => 'v5.4.11',
+ 'version' => '5.4.11.0',
+ 'reference' => '0b900ca5576ecd59e08c76127e616667cfe427a7',
+ 'type' => 'library',
+ 'install_path' => __DIR__ . '/../symfony/dom-crawler',
+ 'aliases' => array(),
+ 'dev_requirement' => false,
+ ),
'symfony/event-dispatcher' => array(
'pretty_version' => 'v4.4.30',
'version' => '4.4.30.0',
diff --git a/composer/platform_check.php b/composer/platform_check.php
index 92370c5a..580fa960 100644
--- a/composer/platform_check.php
+++ b/composer/platform_check.php
@@ -4,8 +4,8 @@
$issues = array();
-if (!(PHP_VERSION_ID >= 70300)) {
- $issues[] = 'Your Composer dependencies require a PHP version ">= 7.3.0". You are running ' . PHP_VERSION . '.';
+if (!(PHP_VERSION_ID >= 70400)) {
+ $issues[] = 'Your Composer dependencies require a PHP version ">= 7.4.0". You are running ' . PHP_VERSION . '.';
}
if ($issues) {
diff --git a/fusonic/linq/LICENSE b/fusonic/linq/LICENSE
new file mode 100644
index 00000000..1617dcba
--- /dev/null
+++ b/fusonic/linq/LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) 2014 Fusonic GmbH (http://www.fusonic.net)
+
+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. \ No newline at end of file
diff --git a/fusonic/linq/src/Fusonic/Linq/GroupedLinq.php b/fusonic/linq/src/Fusonic/Linq/GroupedLinq.php
new file mode 100644
index 00000000..2a2bb510
--- /dev/null
+++ b/fusonic/linq/src/Fusonic/Linq/GroupedLinq.php
@@ -0,0 +1,35 @@
+<?php
+
+/*
+ * This file is part of Fusonic-linq.
+ *
+ * (c) Fusonic GmbH
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Fusonic\Linq;
+
+use Fusonic\Linq\Linq;
+
+/**
+ * Class GroupedLinq
+ * Represents a Linq object that groups together other elements with a group key().
+ * @package Fusonic\Linq
+ */
+class GroupedLinq extends Linq
+{
+ private $groupKey;
+
+ public function __construct($groupKey, $dataSource)
+ {
+ parent::__construct($dataSource);
+ $this->groupKey = $groupKey;
+ }
+
+ public function key()
+ {
+ return $this->groupKey;
+ }
+} \ No newline at end of file
diff --git a/fusonic/linq/src/Fusonic/Linq/Helper/LinqHelper.php b/fusonic/linq/src/Fusonic/Linq/Helper/LinqHelper.php
new file mode 100644
index 00000000..f3e8c9ae
--- /dev/null
+++ b/fusonic/linq/src/Fusonic/Linq/Helper/LinqHelper.php
@@ -0,0 +1,67 @@
+<?php
+
+/*
+ * This file is part of Fusonic-linq.
+ * https://github.com/fusonic/fusonic-linq
+ *
+ * (c) Fusonic GmbH
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Fusonic\Linq\Helper;
+
+use ArrayIterator;
+use InvalidArgumentException;
+use UnexpectedValueException;
+
+class LinqHelper
+{
+ const MODE_ASSERT = 'MODE_ASSERT';
+ const MODE_NULL = 'MODE_NULL';
+
+ const LINQ_ORDER_ASC = 'asc';
+ const LINQ_ORDER_DESC = 'desc';
+
+ const LINQ_ORDER_TYPE_NUMERIC = 1;
+ const LINQ_ORDER_TYPE_ALPHANUMERIC = 2;
+ const LINQ_ORDER_TYPE_DATETIME = 3;
+
+ public static function getBoolOrThrowException($returned)
+ {
+ if (!is_bool($returned)) {
+ throw new UnexpectedValueException("Return type of filter func must be boolean.");
+ }
+ return $returned;
+ }
+
+ public static function assertArgumentIsIterable($param, $argumentName)
+ {
+ if (!self::isIterable($param)) {
+ throw new InvalidArgumentException("Argument must be an array, or implement either the \IteratorAggregate or \Iterator interface. ArgumentName = " . $argumentName);
+ }
+ }
+
+ public static function getIteratorOrThrow($value)
+ {
+ if (is_array($value)) {
+ return new ArrayIterator($value);
+ }
+ else if($value instanceof \IteratorAggregate) {
+ return $value->getIterator();
+ }
+ else if($value instanceof \Iterator) {
+ return $value;
+ }
+
+ throw new \UnexpectedValueException("Value must be an array, or implement either the \IteratorAggregate or \Iterator interface");
+ }
+
+ public static function isIterable($param)
+ {
+ return is_array($param)
+ || $param instanceof \IteratorAggregate
+ || $param instanceof \Iterator;
+ }
+} \ No newline at end of file
diff --git a/fusonic/linq/src/Fusonic/Linq/Iterator/DistinctIterator.php b/fusonic/linq/src/Fusonic/Linq/Iterator/DistinctIterator.php
new file mode 100644
index 00000000..7651fbfd
--- /dev/null
+++ b/fusonic/linq/src/Fusonic/Linq/Iterator/DistinctIterator.php
@@ -0,0 +1,62 @@
+<?php
+
+/*
+ * This file is part of Fusonic-linq.
+ * https://github.com/fusonic/fusonic-linq
+ *
+ * (c) Fusonic GmbH
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Fusonic\Linq\Iterator;
+
+use Iterator;
+
+class DistinctIterator extends \IteratorIterator
+{
+ private $iterator;
+ private $distinct;
+
+ public function __construct(Iterator $iterator)
+ {
+ $this->iterator = $iterator;
+ }
+
+ public function current()
+ {
+ return $this->distinct->current();
+ }
+
+ public function next()
+ {
+ $this->distinct->next();
+ }
+
+ public function key()
+ {
+ return $this->distinct->key();
+ }
+
+ public function valid()
+ {
+ return $this->distinct->valid();
+ }
+
+ public function rewind()
+ {
+ if ($this->distinct === null) {
+ $this->getDistincts();
+ }
+
+ $this->distinct->rewind();
+ }
+
+ private function getDistincts()
+ {
+ $data = iterator_to_array($this->iterator);
+ $distinct = array_unique($data);
+ $this->distinct = new \ArrayIterator($distinct);
+ }
+}
diff --git a/fusonic/linq/src/Fusonic/Linq/Iterator/ExceptIterator.php b/fusonic/linq/src/Fusonic/Linq/Iterator/ExceptIterator.php
new file mode 100644
index 00000000..18d3c2ea
--- /dev/null
+++ b/fusonic/linq/src/Fusonic/Linq/Iterator/ExceptIterator.php
@@ -0,0 +1,65 @@
+<?php
+
+/*
+ * This file is part of Fusonic-linq.
+ * https://github.com/fusonic/fusonic-linq
+ *
+ * (c) Fusonic GmbH
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Fusonic\Linq\Iterator;
+
+use ArrayIterator;
+use Iterator;
+
+class ExceptIterator implements Iterator
+{
+ private $first;
+ private $second;
+ private $result;
+
+ public function __construct(Iterator $first, Iterator $second)
+ {
+ $this->first = $first;
+ $this->second = $second;
+ }
+
+ public function current()
+ {
+ return $this->result->current();
+ }
+
+ public function next()
+ {
+ $this->result->next();
+ }
+
+ public function key()
+ {
+ return $this->result->key();
+ }
+
+ public function valid()
+ {
+ return $this->result->valid();
+ }
+
+ public function rewind()
+ {
+ if ($this->result === null) {
+ $this->getResult();
+ }
+
+ $this->result->rewind();
+ }
+
+ private function getResult()
+ {
+ $firstArray = iterator_to_array($this->first);
+ $secondArray = iterator_to_array($this->second);
+ $this->result = new ArrayIterator(array_diff($firstArray, $secondArray));
+ }
+}
diff --git a/fusonic/linq/src/Fusonic/Linq/Iterator/GroupIterator.php b/fusonic/linq/src/Fusonic/Linq/Iterator/GroupIterator.php
new file mode 100644
index 00000000..0b250179
--- /dev/null
+++ b/fusonic/linq/src/Fusonic/Linq/Iterator/GroupIterator.php
@@ -0,0 +1,74 @@
+<?php
+
+/*
+ * This file is part of Fusonic-linq.
+ * https://github.com/fusonic/fusonic-linq
+ *
+ * (c) Fusonic GmbH
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Fusonic\Linq\Iterator;
+
+use Iterator;
+use ArrayIterator;
+use Fusonic\Linq\GroupedLinq;
+
+class GroupIterator implements Iterator
+{
+ private $iterator;
+ private $grouped;
+ private $keySelector;
+
+ public function __construct($iterator, $keySelector)
+ {
+ $this->iterator = $iterator;
+ $this->keySelector = $keySelector;
+ }
+
+ public function current()
+ {
+ $current = $this->grouped->current();
+ return new GroupedLinq($current['key'], new \ArrayIterator($current['values']));
+ }
+
+ public function next()
+ {
+ $this->grouped->next();
+ }
+
+ public function key()
+ {
+ return $this->grouped->key();
+ }
+
+ public function valid()
+ {
+ return $this->grouped->valid();
+ }
+
+ public function rewind()
+ {
+ if ($this->grouped === null) {
+ $this->doGroup();
+ }
+
+ $this->grouped->rewind();
+ }
+
+ private function doGroup()
+ {
+ $keySelector = $this->keySelector;
+ $this->grouped = new \ArrayIterator(array());
+ foreach ($this->iterator as $value) {
+ $key = $keySelector($value);
+ if (!isset($this->grouped[$key])) {
+ $this->grouped[$key] = array('key' => $key, 'values'=> array());
+ }
+
+ $this->grouped[$key]['values'][] = $value;
+ }
+ }
+}
diff --git a/fusonic/linq/src/Fusonic/Linq/Iterator/IntersectIterator.php b/fusonic/linq/src/Fusonic/Linq/Iterator/IntersectIterator.php
new file mode 100644
index 00000000..d08c2efe
--- /dev/null
+++ b/fusonic/linq/src/Fusonic/Linq/Iterator/IntersectIterator.php
@@ -0,0 +1,65 @@
+<?php
+
+/*
+ * This file is part of Fusonic-linq.
+ * https://github.com/fusonic/fusonic-linq
+ *
+ * (c) Fusonic GmbH
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Fusonic\Linq\Iterator;
+
+use ArrayIterator;
+use Iterator;
+
+class IntersectIterator implements Iterator
+{
+ private $first;
+ private $second;
+ private $intersections;
+
+ public function __construct(Iterator $first, Iterator $second)
+ {
+ $this->first = $first;
+ $this->second = $second;
+ }
+
+ public function current()
+ {
+ return $this->intersections->current();
+ }
+
+ public function next()
+ {
+ $this->intersections->next();
+ }
+
+ public function key()
+ {
+ return $this->intersections->key();
+ }
+
+ public function valid()
+ {
+ return $this->intersections->valid();
+ }
+
+ public function rewind()
+ {
+ if ($this->intersections === null) {
+ $this->calcIntersections();
+ }
+
+ $this->intersections->rewind();
+ }
+
+ private function calcIntersections()
+ {
+ $firstArray = iterator_to_array($this->first);
+ $secondArray = iterator_to_array($this->second);
+ $this->intersections = new ArrayIterator(array_intersect($firstArray, $secondArray));
+ }
+}
diff --git a/fusonic/linq/src/Fusonic/Linq/Iterator/OfTypeIterator.php b/fusonic/linq/src/Fusonic/Linq/Iterator/OfTypeIterator.php
new file mode 100644
index 00000000..46a59482
--- /dev/null
+++ b/fusonic/linq/src/Fusonic/Linq/Iterator/OfTypeIterator.php
@@ -0,0 +1,94 @@
+<?php
+/*
+ * This file is part of Fusonic-linq.
+ * https://github.com/fusonic/fusonic-linq
+ *
+ * (c) Burgy Benjamin <benjamin.burgy@gmail.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Fusonic\Linq\Iterator;
+
+use FilterIterator;
+use Fusonic\Linq\Helper;
+use Iterator;
+
+/**
+ * Iterator for filtering the Linq query with a specified <b>type</b>.
+ * @package Fusonic\Linq\Iterator
+ */
+final class OfTypeIterator
+ extends
+ \FilterIterator
+{
+ /**
+ * @var callable $acceptCallback
+ */
+ private $acceptCallback;
+
+ /**
+ * Initializes an instance of <b>OfTypeIterator</b>.
+ *
+ * @param Iterator $iterator
+ * @param string $type
+ */
+ public function __construct(Iterator $iterator, $type)
+ {
+ parent::__construct($iterator);
+
+ switch (strtolower($type))
+ {
+ case 'int':
+ case 'integer':
+ $this->acceptCallback = function ($current)
+ {
+ return is_int($current);
+ };
+ break;
+ case 'float':
+ case 'double':
+ $this->acceptCallback = function ($current)
+ {
+ return is_float($current);
+ };
+ break;
+ case 'string':
+ $this->acceptCallback = function ($current)
+ {
+ return is_string($current);
+ };
+ break;
+ case 'bool':
+ case 'boolean':
+ $this->acceptCallback = function ($current)
+ {
+ return is_bool($current);
+ };
+ break;
+
+ default:
+ $this->acceptCallback = function ($current) use ($type)
+ {
+ return $current instanceof $type;
+ };
+ }
+ }
+
+ /**
+ * (PHP 5 &gt;= 5.1.0)<br/>
+ * Check whether the current element of the iterator is acceptable
+ * @link http://php.net/manual/en/filteriterator.accept.php
+ * @return bool true if the current element is acceptable, otherwise false.
+ */
+ public function accept()
+ {
+ /** @var mixed $current */
+ $current = $this->current();
+ /** @var callable $func */
+ $func = $this->acceptCallback;
+
+ return $func($current);
+ }
+} \ No newline at end of file
diff --git a/fusonic/linq/src/Fusonic/Linq/Iterator/OrderIterator.php b/fusonic/linq/src/Fusonic/Linq/Iterator/OrderIterator.php
new file mode 100644
index 00000000..732e79eb
--- /dev/null
+++ b/fusonic/linq/src/Fusonic/Linq/Iterator/OrderIterator.php
@@ -0,0 +1,113 @@
+<?php
+
+/*
+ * This file is part of Fusonic-linq.
+ * https://github.com/fusonic/fusonic-linq
+ *
+ * (c) Fusonic GmbH
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Fusonic\Linq\Iterator;
+
+use Iterator;
+use ArrayIterator;
+use Fusonic\Linq;
+use Fusonic\Linq\Helper;
+
+class OrderIterator implements Iterator
+{
+ private $iterator;
+ private $direction;
+ private $orderedIterator;
+ private $orderKeyFunc;
+
+ public function __construct(Iterator $items, $orderKeyFunc, $direction)
+ {
+ $this->iterator = $items;
+ $this->direction = $direction;
+ $this->orderKeyFunc = $orderKeyFunc;
+ }
+
+ public function current()
+ {
+ return $this->orderedIterator->current();
+ }
+
+ public function next()
+ {
+ $this->orderedIterator->next();
+ }
+
+ public function key()
+ {
+ return $this->orderedIterator->key();
+ }
+
+ public function valid()
+ {
+ return $this->orderedIterator->valid();
+ }
+
+ public function rewind()
+ {
+ if ($this->orderedIterator == null) {
+ $this->orderItems();
+ }
+ $this->orderedIterator->rewind();
+ }
+
+ public function orderItems()
+ {
+ $orderKeyFunc = $this->orderKeyFunc;
+ $direction = $this->direction;
+
+ $itemIterator = $this->iterator;
+ $itemIterator->rewind();
+ if (!$itemIterator->valid()) {
+ $this->orderedIterator = new ArrayIterator();
+ return;
+ }
+
+ $firstOrderKey = $orderKeyFunc($itemIterator->current());
+
+ $sortType = Helper\LinqHelper::LINQ_ORDER_TYPE_NUMERIC;
+
+ if ($firstOrderKey instanceof \DateTime) {
+ $sortType = Helper\LinqHelper::LINQ_ORDER_TYPE_DATETIME;
+ } elseif (!is_numeric($firstOrderKey)) {
+ $sortType = Helper\LinqHelper::LINQ_ORDER_TYPE_ALPHANUMERIC;
+ }
+
+ $keyMap = array();
+ $valueMap = array();
+
+ foreach ($itemIterator as $value) {
+ $orderKey = $orderKeyFunc != null ? $orderKeyFunc($value) : $value;
+ if ($sortType == Helper\LinqHelper::LINQ_ORDER_TYPE_DATETIME) {
+ $orderKey = $orderKey->getTimeStamp();
+ }
+ $keyMap[] = $orderKey;
+ $valueMap[] = $value;
+ }
+
+ if ($sortType == Helper\LinqHelper::LINQ_ORDER_TYPE_DATETIME) {
+ $sortType = Helper\LinqHelper::LINQ_ORDER_TYPE_NUMERIC;
+ }
+
+ if ($direction == Helper\LinqHelper::LINQ_ORDER_ASC) {
+ asort($keyMap, $sortType == Helper\LinqHelper::LINQ_ORDER_TYPE_NUMERIC ? SORT_NUMERIC : SORT_LOCALE_STRING);
+ } else {
+ arsort($keyMap, $sortType == Helper\LinqHelper::LINQ_ORDER_TYPE_NUMERIC ? SORT_NUMERIC : SORT_LOCALE_STRING);
+ }
+
+ $sorted = new ArrayIterator(array());
+ foreach ($keyMap as $key => $value) {
+ $sorted[] = $valueMap[$key];
+ }
+
+ $this->orderedIterator = $sorted;
+ }
+}
diff --git a/fusonic/linq/src/Fusonic/Linq/Iterator/SelectIterator.php b/fusonic/linq/src/Fusonic/Linq/Iterator/SelectIterator.php
new file mode 100644
index 00000000..aa698b3d
--- /dev/null
+++ b/fusonic/linq/src/Fusonic/Linq/Iterator/SelectIterator.php
@@ -0,0 +1,37 @@
+<?php
+
+/*
+ * This file is part of Fusonic-linq.
+ * https://github.com/fusonic/fusonic-linq
+ *
+ * (c) Fusonic GmbH
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Fusonic\Linq\Iterator;
+
+use InvalidArgumentException;
+use Iterator;
+
+class SelectIterator extends \IteratorIterator
+{
+ private $selector;
+
+ public function __construct(Iterator $iterator, $selector)
+ {
+ parent::__construct($iterator);
+ if ($selector === null) {
+ throw new InvalidArgumentException("Selector must not be null.");
+ }
+
+ $this->selector = $selector;
+ }
+
+ public function current()
+ {
+ $selector = $this->selector;
+ return $selector(parent::current());
+ }
+}
diff --git a/fusonic/linq/src/Fusonic/Linq/Iterator/SelectManyIterator.php b/fusonic/linq/src/Fusonic/Linq/Iterator/SelectManyIterator.php
new file mode 100644
index 00000000..310b8bdb
--- /dev/null
+++ b/fusonic/linq/src/Fusonic/Linq/Iterator/SelectManyIterator.php
@@ -0,0 +1,84 @@
+<?php
+
+/*
+ * This file is part of Fusonic-linq.
+ * https://github.com/fusonic/fusonic-linq
+ *
+ * (c) Fusonic GmbH
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Fusonic\Linq\Iterator;
+
+use Iterator;
+use Fusonic\Linq\Helper;
+
+class SelectManyIterator implements Iterator
+{
+ private $iterator;
+ private $currentIterator;
+ private $key = 0;
+
+ public function __construct(Iterator $iterator)
+ {
+ $this->iterator = $iterator;
+ }
+
+ public function current()
+ {
+ if ($this->currentIterator != null) {
+ return $this->currentIterator->current();
+ }
+
+ return null;
+ }
+
+ public function next()
+ {
+ if ($this->currentIterator != null) {
+ $this->currentIterator->next();
+
+ if (!$this->currentIterator->valid()) {
+ $this->iterator->next();
+ if ($this->iterator->valid()) {
+ $this->currentIterator = Helper\LinqHelper::getIteratorOrThrow($this->iterator->current());
+ if ($this->currentIterator != null) {
+ $this->currentIterator->rewind();
+ $this->key++;
+ }
+ }
+ } else {
+ $this->key++;
+ }
+ }
+ }
+
+ public function key()
+ {
+ return $this->key;
+ }
+
+ public function valid()
+ {
+ $current = $this->currentIterator;
+ return $current != null && $current->valid();
+ }
+
+ public function rewind()
+ {
+ $this->iterator->rewind();
+ if ($this->iterator->valid()) {
+ $current = $this->iterator->current();
+ $this->currentIterator = Helper\LinqHelper::getIteratorOrThrow($current);
+ if ($this->currentIterator != null) {
+ $this->currentIterator->rewind();
+ }
+ } else {
+ $this->currentIterator = null;
+ }
+
+ $this->key = 0;
+ }
+} \ No newline at end of file
diff --git a/fusonic/linq/src/Fusonic/Linq/Iterator/WhereIterator.php b/fusonic/linq/src/Fusonic/Linq/Iterator/WhereIterator.php
new file mode 100644
index 00000000..1a757e00
--- /dev/null
+++ b/fusonic/linq/src/Fusonic/Linq/Iterator/WhereIterator.php
@@ -0,0 +1,34 @@
+<?php
+
+/*
+ * This file is part of Fusonic-linq.
+ * https://github.com/fusonic/fusonic-linq
+ *
+ * (c) Fusonic GmbH
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Fusonic\Linq\Iterator;
+
+use Iterator;
+use Fusonic\Linq\Helper;
+
+class WhereIterator extends \FilterIterator
+{
+ private $func;
+
+ public function __construct(Iterator $iterator, $func)
+ {
+ parent::__construct($iterator);
+ $this->func = $func;
+ }
+
+ public function accept()
+ {
+ $func = $this->func;
+ $current = $this->current();
+ return Helper\LinqHelper::getBoolOrThrowException($func($current));
+ }
+} \ No newline at end of file
diff --git a/fusonic/linq/src/Fusonic/Linq/Linq.php b/fusonic/linq/src/Fusonic/Linq/Linq.php
new file mode 100644
index 00000000..f9a56810
--- /dev/null
+++ b/fusonic/linq/src/Fusonic/Linq/Linq.php
@@ -0,0 +1,732 @@
+<?php
+
+/*
+ * This file is part of Fusonic-linq.
+ *
+ * (c) Fusonic GmbH
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Fusonic\Linq;
+
+use Countable;
+use Fusonic\Linq\Iterator\ExceptIterator;
+use Fusonic\Linq\Iterator\DistinctIterator;
+use Fusonic\Linq\Iterator\GroupIterator;
+use Fusonic\Linq\Iterator\IntersectIterator;
+use Fusonic\Linq\Iterator\OfTypeIterator;
+use Fusonic\Linq\Iterator\OrderIterator;
+use Fusonic\Linq\Iterator\SelectIterator;
+use Fusonic\Linq\Iterator\SelectManyIterator;
+use Fusonic\Linq\Iterator\WhereIterator;
+use Fusonic\Linq\Helper\LinqHelper;
+use IteratorAggregate;
+use Traversable;
+use UnexpectedValueException;
+use InvalidArgumentException;
+use OutOfRangeException;
+
+/**
+ * Linq is a simple, powerful and consistent library for querying, projecting and aggregating data in php.
+ *
+ * @author David Roth <david.roth@fusonic.net>.
+ */
+class Linq implements IteratorAggregate, Countable
+{
+ private $iterator;
+
+ /**
+ * Creates a new Linq object using the provided dataSource.
+ *
+ * @param array|\Iterator|IteratorAggregate $dataSource A Traversable sequence as data source.
+ */
+ public function __construct($dataSource)
+ {
+ LinqHelper::assertArgumentIsIterable($dataSource, "dataSource");
+ $dataSource = LinqHelper::getIteratorOrThrow($dataSource);
+
+ $this->iterator = $dataSource;
+ }
+
+ /**
+ * Creates a new Linq object using the provided dataDataSource.
+ * This is the recommended way for getting a new Linq instance.
+ *
+ * @param array|\Iterator|IteratorAggregate $dataSource A Traversable sequence as data source.
+ * @return Linq
+ */
+ public static function from($dataSource)
+ {
+ return new Linq($dataSource);
+ }
+
+ /**
+ * Generates a sequence of integral numbers within a specified range.
+ *
+ * @param $start The value of the first integer in the sequence.
+ * @param $count The number of sequential integers to generate.
+ * @return Linq An sequence that contains a range of sequential int numbers.
+ * @throws \OutOfRangeException
+ */
+ public static function range($start, $count)
+ {
+ if ($count < 0) {
+ throw new OutOfRangeException('$count must be not be negative.');
+ }
+
+ return new Linq(range($start, $start + $count - 1));
+ }
+
+ /**
+ * Filters the Linq object according to func return result.
+ *
+ * @param callback $func A func that returns boolean
+ * @return Linq Filtered results according to $func
+ */
+ public function where($func)
+ {
+ return new Linq(new WhereIterator($this->iterator, $func));
+ }
+
+ /**
+ * Filters the Linq object according to type.
+ *
+ * @param string $type
+ *
+ * @return Linq Filtered results according to $func
+ */
+ public function ofType($type)
+ {
+ return new Linq(new OfTypeIterator($this->iterator, $type));
+ }
+
+ /**
+ * Bypasses a specified number of elements and then returns the remaining elements.
+ *
+ * @param int $count The number of elements to skip before returning the remaining elements.
+ * @return Linq A Linq object that contains the elements that occur after the specified index.
+ */
+ public function skip($count)
+ {
+ // If its an array iterator we must check the arrays bounds are greater than the skip count.
+ // This is because the LimitIterator will use the seek() method which will throw an exception if $count > array.bounds.
+ $innerIterator = $this->iterator;
+ if ($innerIterator instanceof \ArrayIterator) {
+ if ($count >= $innerIterator->count()) {
+ return new Linq(array());
+ }
+ }
+
+ return new Linq(new \LimitIterator($innerIterator, $count, -1));
+ }
+
+ /**
+ * Returns a specified number of contiguous elements from the start of a sequence
+ *
+ * @param int $count The number of elements to return.
+ * @return Linq A Linq object that contains the specified number of elements from the start.
+ */
+ public function take($count)
+ {
+ if ($count == 0) {
+ return new Linq(array());
+ }
+
+ return new Linq(new \LimitIterator($this->iterator, 0, $count));
+ }
+
+ /**
+ * Applies an accumulator function over a sequence.
+ * The aggregate method makes it simple to perform a calculation over a sequence of values.
+ * This method works by calling $func one time for each element.
+ * The first element of source is used as the initial aggregate value if $seed parameter is not specified.
+ * If $seed is specified, this value will be used as the first value.
+ *
+ * @param callback $func An accumulator function to be invoked on each element.
+ * @param mixed $seed
+ * @throws \RuntimeException if the input sequence contains no elements.
+ * @return mixed Returns the final result of $func.
+ */
+ public function aggregate($func, $seed = null)
+ {
+ $result = null;
+ $first = true;
+
+ if ($seed !== null) {
+ $result = $seed;
+ $first = false;
+ }
+
+ foreach ($this->iterator as $current) {
+ if (!$first) {
+ $result = $func($result, $current);
+ } else {
+ $result = $current;
+ $first = false;
+ }
+ }
+ if ($first) {
+ throw new \RuntimeException("The input sequence contains no elements.");
+ }
+ return $result;
+ }
+
+ /**
+ * Splits the sequence in chunks according to $chunksize.
+ *
+ * @param $chunksize Specifies how many elements are grouped together per chunk.
+ * @throws \InvalidArgumentException
+ * @return Linq
+ */
+ public function chunk($chunksize)
+ {
+ if ($chunksize < 1) {
+ throw new \InvalidArgumentException("chunksize", $chunksize);
+ }
+
+ $i = -1;
+ return $this->select(
+ function ($x) use (&$i) {
+ $i++;
+ return array("index" => $i, "value" => $x);
+ }
+ )
+ ->groupBy(
+ function ($pair) use ($chunksize) {
+ return $pair["index"] / $chunksize;
+ }
+ )
+ ->select(
+ function (GroupedLinq $group) {
+ return $group->select(
+ function ($v) {
+ return $v["value"];
+ }
+ );
+ }
+ );
+ }
+
+ /**
+ * Determines whether all elements satisfy a condition.
+ *
+ * @param callback $func A function to test each element for a condition.
+ * @return bool True if every element passes the test in the specified func, or if the sequence is empty; otherwise, false.
+ */
+ public function all($func)
+ {
+ foreach ($this->iterator as $current) {
+ $match = LinqHelper::getBoolOrThrowException($func($current));
+ if (!$match) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Determines whether any element exists or satisfies a condition by invoking $func.
+ *
+ * @param callback $func A function to test each element for a condition or NULL to determine if any element exists.
+ * @return bool True if no $func given and the source sequence contains any elements or True if any elements passed the test in the specified func; otherwise, false.
+ */
+ public function any($func = null)
+ {
+ foreach ($this->iterator as $current) {
+ if ($func === null) {
+ return true;
+ }
+
+ $match = LinqHelper::getBoolOrThrowException($func($current));
+ if ($match) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Counts the elements of this Linq sequence.
+ * @return int
+ */
+ public function count()
+ {
+ if ($this->iterator instanceof Countable) {
+ return $this->iterator->count();
+ }
+
+ return iterator_count($this->iterator);
+ }
+
+ /**
+ * Computes the average of all numeric values. Uses $func to obtain the value on each element.
+ *
+ * @param callback $func A func that returns any numeric type (int, float etc.)
+ * @throws \UnexpectedValueException if an item of the sequence is not a numeric value.
+ * @return double Average of items
+ */
+ public function average($func = null)
+ {
+ $resultTotal = 0;
+ $itemCount = 0;
+
+ $source = $this->getSelectIteratorOrInnerIterator($func);
+
+ foreach ($source as $item) {
+ if (!is_numeric($item)) {
+ throw new UnexpectedValueException("Cannot calculate an average on a none numeric value");
+ }
+
+ $resultTotal += $item;
+ $itemCount++;
+ }
+ return $itemCount == 0 ? 0 : ($resultTotal / $itemCount);
+ }
+
+ /**
+ * Sorts the elements in ascending order according to a key provided by $func.
+ *
+ * @param callback $func A function to extract a key from an element.
+ * @return Linq A new Linq instance whose elements are sorted ascending according to a key.
+ */
+ public function orderBy($func)
+ {
+ return $this->order($func, LinqHelper::LINQ_ORDER_ASC);
+ }
+
+ /**
+ * Sorts the elements in descending order according to a key provided by $func.
+ *
+ * @param callback $func A function to extract a key from an element.
+ * @return Linq A new Linq instance whose elements are sorted descending according to a key.
+ */
+ public function orderByDescending($func)
+ {
+ return $this->order($func, LinqHelper::LINQ_ORDER_DESC);
+ }
+
+ private function order($func, $direction = LinqHelper::LINQ_ORDER_ASC)
+ {
+ return new Linq(new OrderIterator($this->iterator, $func, $direction));
+ }
+
+ /**
+ * Gets the sum of all items or by invoking a transform function on each item to get a numeric value.
+ *
+ * @param callback $func A func that returns any numeric type (int, float etc.) from the given element, or NULL to use the element itself.
+ * @throws \UnexpectedValueException if any element is not a numeric value.
+ * @return double The sum of all items.
+ */
+ public function sum($func = null)
+ {
+ $sum = 0;
+ $iterator = $this->getSelectIteratorOrInnerIterator($func);
+ foreach ($iterator as $value) {
+ if (!is_numeric($value)) {
+ throw new UnexpectedValueException("sum() only works on numeric values.");
+ }
+
+ $sum += $value;
+ }
+ return $sum;
+ }
+
+ /**
+ * Gets the minimum item value of all items or by invoking a transform function on each item to get a numeric value.
+ *
+ * @param callback $func A func that returns any numeric type (int, float etc.) from the given element, or NULL to use the element itself.
+ * @throws \RuntimeException if the sequence contains no elements
+ * @throws \UnexpectedValueException
+ * @return double Minimum item value
+ */
+ public function min($func = null)
+ {
+ $min = null;
+ $iterator = $this->getSelectIteratorOrInnerIterator($func);
+ foreach ($iterator as $value) {
+ if (!is_numeric($value) && !is_string($value) && !($value instanceof \DateTime)) {
+ throw new UnexpectedValueException("min() only works on numeric values, strings and DateTime objects.");
+ }
+
+ if (is_null($min)) {
+ $min = $value;
+ } elseif ($min > $value) {
+ $min = $value;
+ }
+ }
+
+ if ($min === null) {
+ throw new \RuntimeException("Cannot calculate min() as the Linq sequence contains no elements.");
+ }
+
+ return $min;
+ }
+
+ /**
+ * Returns the maximum item value according to $func
+ *
+ * @param callback $func A func that returns any numeric type (int, float etc.)
+ * @throws \RuntimeException if the sequence contains no elements
+ * @throws \UnexpectedValueException if any element is not a numeric value or a string.
+ * @return double Maximum item value
+ */
+ public function max($func = null)
+ {
+ $max = null;
+ $iterator = $this->getSelectIteratorOrInnerIterator($func);
+ foreach ($iterator as $value) {
+ if (!is_numeric($value) && !is_string($value) && !($value instanceof \DateTime)) {
+ throw new UnexpectedValueException("max() only works on numeric values, strings and DateTime objects.");
+ }
+
+ if (is_null($max)) {
+ $max = $value;
+ } elseif ($max < $value) {
+ $max = $value;
+ }
+ }
+
+ if ($max === null) {
+ throw new \RuntimeException("Cannot calculate max() as the Linq sequence contains no elements.");
+ }
+
+ return $max;
+ }
+
+ /**
+ * Projects each element into a new form by invoking the selector function.
+ *
+ * @param callback $func A transform function to apply to each element.
+ * @return Linq A new Linq object whose elements are the result of invoking the transform function on each element of the original Linq object.
+ */
+ public function select($func)
+ {
+ return new Linq(new SelectIterator($this->iterator, $func));
+ }
+
+ /**
+ * Projects each element of a sequence to a new Linq and flattens the resulting sequences into one sequence.
+ *
+ * @param callback $func A func that returns a sequence (array, Linq, Iterator).
+ * @throws \UnexpectedValueException if an element is not a traversable sequence.
+ * @return Linq A new Linq object whose elements are the result of invoking the one-to-many transform function on each element of the input sequence.
+ */
+ public function selectMany($func)
+ {
+ return new Linq(new SelectManyIterator(new SelectIterator($this->iterator, $func)));
+ }
+
+ /**
+ * Performs the specified action on each element of the Linq sequence and returns the Linq sequence.
+ * @param callback $func A func that will be evaluated for each item in the linq sequence.
+ * @return Linq The original Linq sequence that was used to perform the foreach.
+ */
+ public function each($func)
+ {
+ foreach ($this->iterator as $item) {
+ $func($item);
+ }
+ return $this;
+ }
+
+ /**
+ * Determines whether a sequence contains a specified element.
+ * This function will use php strict comparison (===). If you need custom comparison use the Linq::any($func) method.
+ *
+ * @param mixed $value The value to locate in the sequence.
+ * @return bool True if $value is found within the sequence; otherwise false.
+ */
+ public function contains($value)
+ {
+ return $this->any(
+ function ($x) use ($value) {
+ return $x === $value;
+ }
+ );
+ }
+
+ /**
+ * Concatenates this Linq object with the given sequence.
+ *
+ * @param array|\Iterator $second A sequence which will be concatenated with this Linq object.
+ * @throws InvalidArgumentException if the given sequence is not traversable.
+ * @return Linq A new Linq object that contains the concatenated elements of the input sequence and the original Linq sequence.
+ */
+ public function concat($second)
+ {
+ LinqHelper::assertArgumentIsIterable($second, "second");
+
+ $allItems = new \ArrayIterator(array($this->iterator, $second));
+
+ return new Linq(new SelectManyIterator($allItems));
+ }
+
+ /**
+ * Returns distinct item values of this
+ *
+ * @param callback $func
+ * @return Linq Distinct item values of this
+ */
+ public function distinct($func = null)
+ {
+ return new Linq(new DistinctIterator($this->getSelectIteratorOrInnerIterator($func)));
+ }
+
+ /**
+ * Intersects the Linq sequence with second Iterable sequence.
+ *
+ * @param \Iterator|array An iterator to intersect with:
+ * @return Linq intersected items
+ */
+ public function intersect($second)
+ {
+ LinqHelper::assertArgumentIsIterable($second, "second");
+ return new Linq(new IntersectIterator($this->iterator, LinqHelper::getIteratorOrThrow($second)));
+ }
+
+ /**
+ * Returns all elements except the ones of the given sequence.
+ *
+ * @param array|\Iterator $second
+ * @return Linq Returns all items of this not occuring in $second
+ */
+ public function except($second)
+ {
+ LinqHelper::assertArgumentIsIterable($second, "second");
+ return new Linq(new ExceptIterator($this->iterator, LinqHelper::getIteratorOrThrow($second)));
+ }
+
+ /**
+ * Returns the element at a specified index.
+ * This method throws an exception if index is out of range.
+ * To instead return NULL when the specified index is out of range, use the elementAtOrNull method.
+ *
+ * @throws \OutOfRangeException if index is less than 0 or greater than or equal to the number of elements in the sequence.
+ * @param int $index
+ * @return mixed Item at $index
+ */
+ public function elementAt($index)
+ {
+ return $this->getValueAt($index, true);
+ }
+
+ /**
+ * Returns the element at a specified index or NULL if the index is out of range.
+ *
+ * @param $index
+ * @return mixed Item at $index
+ */
+ public function elementAtOrNull($index)
+ {
+ return $this->getValueAt($index, false);
+ }
+
+ private function getValueAt($index, $throwEx)
+ {
+ $i = 0;
+ foreach ($this->iterator as $value) {
+ if ($i == $index) {
+ return $value;
+ }
+ $i++;
+ }
+
+ if ($throwEx) {
+ throw new OutOfRangeException("Index is less than 0 or greater than or equal to the number of elements in the sequence.");
+ }
+
+ return null;
+ }
+
+ /**
+ * Groups the object according to the $func generated key
+ *
+ * @param callback $keySelector a func that returns an item as key, item can be any type.
+ * @return GroupedLinq
+ */
+ public function groupBy($keySelector)
+ {
+ return new Linq(new GroupIterator($this->iterator, $keySelector));
+ }
+
+ /**
+ * Returns the last element that satisfies a specified condition.
+ * @throws \RuntimeException if no element satisfies the condition in predicate or the source sequence is empty.
+ *
+ * @param callback $func a func that returns boolean.
+ * @return Object Last item in this
+ */
+ public function last($func = null)
+ {
+ return $this->getLast($func, true);
+ }
+
+ /**
+ * Returns the last element that satisfies a condition or NULL if no such element is found.
+ *
+ * @param callback $func a func that returns boolean.
+ * @return mixed
+ */
+ public function lastOrNull($func = null)
+ {
+ return $this->getLast($func, false);
+ }
+
+ /**
+ * Returns the first element that satisfies a specified condition
+ * @throws \RuntimeException if no element satisfies the condition in predicate -or- the source sequence is empty / does not match any elements.
+ *
+ * @param callback $func a func that returns boolean.
+ * @return mixed
+ */
+ public function first($func = null)
+ {
+ return $this->getFirst($func, true);
+ }
+
+ /**
+ * Returns the first element, or NULL if the sequence contains no elements.
+ *
+ * @param callback $func a func that returns boolean.
+ * @return mixed
+ */
+ public function firstOrNull($func = null)
+ {
+ return $this->getFirst($func, false);
+ }
+
+ /**
+ * Returns the only element that satisfies a specified condition.
+ *
+ * @throws \RuntimeException if no element exists or if more than one element exists.
+ * @param callback $func a func that returns boolean.
+ * @return mixed
+ */
+ public function single($func = null)
+ {
+ return $this->getSingle($func, true);
+ }
+
+ /**
+ * Returns the only element that satisfies a specified condition or NULL if no such element exists.
+ *
+ * @throws \RuntimeException if more than one element satisfies the condition.
+ * @param callback $func a func that returns boolean.
+ * @return mixed
+ */
+ public function singleOrNull($func = null)
+ {
+ return $this->getSingle($func, false);
+ }
+
+
+ private function getWhereIteratorOrInnerIterator($func)
+ {
+ return $func === null ? $this->iterator : new WhereIterator($this->iterator, $func);
+ }
+
+ private function getSelectIteratorOrInnerIterator($func)
+ {
+ return $func === null ? $this->iterator : new SelectIterator($this->iterator, $func);
+ }
+
+ private function getSingle($func, $throw)
+ {
+ $source = $this->getWhereIteratorOrInnerIterator($func);
+
+ $count = 0;
+ $single = null;
+
+ foreach ($source as $stored) {
+ $count++;
+
+ if ($count > 1) {
+ throw new \RuntimeException("The input sequence contains more than 1 elements.");
+ }
+
+ $single = $stored;
+ }
+
+ if ($count == 0 && $throw) {
+ throw new \RuntimeException("The input sequence contains no matching element.");
+ }
+
+ return $single;
+ }
+
+ private function getFirst($func, $throw)
+ {
+ $source = $this->getWhereIteratorOrInnerIterator($func);
+
+ $count = 0;
+ $first = null;
+
+ foreach ($source as $stored) {
+ $count++;
+ $first = $stored;
+ break;
+ }
+
+ if ($count == 0 && $throw) {
+ throw new \RuntimeException("The input sequence contains no matching element.");
+ }
+
+ return $first;
+ }
+
+ private function getLast($func, $throw)
+ {
+ $source = $this->getWhereIteratorOrInnerIterator($func);
+
+ $count = 0;
+ $last = null;
+
+ foreach ($source as $stored) {
+ $count++;
+ $last = $stored;
+ }
+
+ if ($count == 0 && $throw) {
+ throw new \RuntimeException("The input sequence contains no matching element.");
+ }
+
+ return $last;
+ }
+
+ /**
+ * Creates an Array from this Linq object with key/value selector(s).
+ *
+ * @param callback $keySelector a func that returns the array-key for each element.
+ * @param callback $valueSelector a func that returns the array-value for each element.
+ *
+ * @return Array An array with all values.
+ */
+ public function toArray($keySelector = null, $valueSelector = null)
+ {
+ if ($keySelector === null && $valueSelector === null) {
+ return iterator_to_array($this, false);
+ } elseif ($keySelector == null) {
+ return iterator_to_array(new SelectIterator($this->getIterator(), $valueSelector), false);
+ } else {
+ $array = array();
+ foreach ($this as $value) {
+ $key = $keySelector($value);
+ $array[$key] = $valueSelector == null ? $value : $valueSelector($value);
+ }
+ return $array;
+ }
+ }
+
+ /**
+ * Retrieves the iterator of this Linq class.
+ * @link http://php.net/manual/en/iteratoraggregate.getiterator.php
+ * @return Traversable An instance of an object implementing <b>Iterator</b> or
+ * <b>Traversable</b>
+ */
+ public function getIterator()
+ {
+ return $this->iterator;
+ }
+} \ No newline at end of file
diff --git a/fusonic/opengraph/LICENSE b/fusonic/opengraph/LICENSE
new file mode 100644
index 00000000..46c173a3
--- /dev/null
+++ b/fusonic/opengraph/LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) 2014 Fusonic GmbH (http://www.fusonic.net)
+
+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.
diff --git a/fusonic/opengraph/src/Consumer.php b/fusonic/opengraph/src/Consumer.php
new file mode 100644
index 00000000..25b03feb
--- /dev/null
+++ b/fusonic/opengraph/src/Consumer.php
@@ -0,0 +1,156 @@
+<?php
+
+namespace Fusonic\OpenGraph;
+
+use DOMElement;
+use Fusonic\Linq\Linq;
+use Fusonic\OpenGraph\Objects\ObjectBase;
+use Fusonic\OpenGraph\Objects\Website;
+use LogicException;
+use Psr\Http\Client\ClientExceptionInterface;
+use Psr\Http\Client\ClientInterface;
+use Psr\Http\Message\RequestFactoryInterface;
+use Symfony\Component\DomCrawler\Crawler;
+
+/**
+ * Consumer that extracts Open Graph data from either a URL or a HTML string.
+ */
+class Consumer
+{
+ private ?ClientInterface $client;
+ private ?RequestFactoryInterface $requestFactory;
+
+ /**
+ * When enabled, crawler will read content of title and meta description if no
+ * Open Graph data is provided by target page.
+ */
+ public bool $useFallbackMode = false;
+
+ /**
+ * When enabled, crawler will throw exceptions for some crawling errors like unexpected
+ * Open Graph elements.
+ */
+ public bool $debug = false;
+
+ /**
+ * @param ClientInterface|null $client A PSR-18 ClientInterface implementation.
+ * @param RequestFactoryInterface|null $requestFactory A PSR-17 RequestFactoryInterface implementation.
+ */
+ public function __construct(?ClientInterface $client = null, ?RequestFactoryInterface $requestFactory = null)
+ {
+ $this->client = $client;
+ $this->requestFactory = $requestFactory;
+ }
+
+ /**
+ * Fetches HTML content from the given URL and then crawls it for Open Graph data.
+ *
+ * @param string $url URL to be crawled.
+ *
+ * @return ObjectBase
+ *
+ * @throws ClientExceptionInterface
+ */
+ public function loadUrl(string $url): ObjectBase
+ {
+ if ($this->client === null) {
+ throw new LogicException(
+ "To use loadUrl() you must provide \$client and \$requestFactory when instantiating the consumer."
+ );
+ }
+
+ $request = $this->requestFactory->createRequest("GET", $url);
+ $response = $this->client->sendRequest($request);
+
+ return $this->loadHtml($response->getBody()->getContents(), $url);
+ }
+
+ /**
+ * Crawls the given HTML string for OpenGraph data.
+ *
+ * @param string $html HTML string, usually whole content of crawled web resource.
+ * @param string $fallbackUrl URL to use when fallback mode is enabled.
+ *
+ * @return ObjectBase
+ */
+ public function loadHtml(string $html, string $fallbackUrl = null): ObjectBase
+ {
+ // Extract all data that can be found
+ $page = $this->extractOpenGraphData($html);
+
+ // Use the user's URL as fallback
+ if ($this->useFallbackMode && $page->url === null) {
+ $page->url = $fallbackUrl;
+ }
+
+ // Return result
+ return $page;
+ }
+
+ private function extractOpenGraphData(string $content): ObjectBase
+ {
+ $crawler = new Crawler;
+ $crawler->addHTMLContent($content, 'UTF-8');
+
+ $properties = [];
+ foreach(['name', 'property'] as $t)
+ {
+ // Get all meta-tags starting with "og:"
+ $ogMetaTags = $crawler->filter("meta[{$t}^='og:']");
+ // Create clean property array
+ $props = Linq::from($ogMetaTags)
+ ->select(
+ function (DOMElement $tag) use ($t) {
+ $name = strtolower(trim($tag->getAttribute($t)));
+ $value = trim($tag->getAttribute("content"));
+ return new Property($name, $value);
+ }
+ )
+ ->toArray();
+ $properties = array_merge($properties, $props);
+
+ }
+
+ // Create new object of the correct type
+ $typeProperty = Linq::from($properties)
+ ->firstOrNull(
+ function (Property $property) {
+ return $property->key === Property::TYPE;
+ }
+ );
+ switch ($typeProperty !== null ? $typeProperty->value : null) {
+ default:
+ $object = new Website();
+ break;
+ }
+
+ // Assign all properties to the object
+ $object->assignProperties($properties, $this->debug);
+
+ // Fallback for url
+ if ($this->useFallbackMode && !$object->url) {
+ $urlElement = $crawler->filter("link[rel='canonical']")->first();
+ if ($urlElement->count() > 0) {
+ $object->url = trim($urlElement->attr("href"));
+ }
+ }
+
+ // Fallback for title
+ if ($this->useFallbackMode && !$object->title) {
+ $titleElement = $crawler->filter("title")->first();
+ if ($titleElement->count() > 0) {
+ $object->title = trim($titleElement->text());
+ }
+ }
+
+ // Fallback for description
+ if ($this->useFallbackMode && !$object->description) {
+ $descriptionElement = $crawler->filter("meta[property='description']")->first();
+ if ($descriptionElement->count() > 0) {
+ $object->description = trim($descriptionElement->attr("content"));
+ }
+ }
+
+ return $object;
+ }
+}
diff --git a/fusonic/opengraph/src/Elements/Audio.php b/fusonic/opengraph/src/Elements/Audio.php
new file mode 100644
index 00000000..20df532d
--- /dev/null
+++ b/fusonic/opengraph/src/Elements/Audio.php
@@ -0,0 +1,67 @@
+<?php
+
+namespace Fusonic\OpenGraph\Elements;
+
+use Fusonic\OpenGraph\Property;
+
+/**
+ * An Open Graph audio element.
+ */
+class Audio extends ElementBase
+{
+ /**
+ * The URL of an audio resource associated with the object.
+ *
+ * @var string
+ */
+ public $url;
+
+ /**
+ * An alternate URL to use if an audio resource requires HTTPS.
+ *
+ * @var string
+ */
+ public $secureUrl;
+
+ /**
+ * The MIME type of an audio resource associated with the object.
+ *
+ * @var type
+ */
+ public $type;
+
+ /**
+ * @param string $url URL to the audio file.
+ */
+ public function __construct($url)
+ {
+ parent::__construct();
+
+ $this->url = $url;
+ }
+
+ /**
+ * Gets all properties set on this element.
+ *
+ * @return array|Property[]
+ */
+ public function getProperties()
+ {
+ $properties = [];
+
+ // URL must precede all other properties
+ if ($this->url !== null) {
+ $properties[] = new Property(Property::AUDIO_URL, $this->url);
+ }
+
+ if ($this->secureUrl !== null) {
+ $properties[] = new Property(Property::AUDIO_SECURE_URL, $this->secureUrl);
+ }
+
+ if ($this->type !== null) {
+ $properties[] = new Property(Property::AUDIO_TYPE, $this->type);
+ }
+
+ return $properties;
+ }
+}
diff --git a/fusonic/opengraph/src/Elements/ElementBase.php b/fusonic/opengraph/src/Elements/ElementBase.php
new file mode 100644
index 00000000..dd4139dc
--- /dev/null
+++ b/fusonic/opengraph/src/Elements/ElementBase.php
@@ -0,0 +1,20 @@
+<?php
+
+namespace Fusonic\OpenGraph\Elements;
+
+/**
+ * Abstract base class for all OpenGraph elements (e.g. images, videos etc.)
+ */
+abstract class ElementBase
+{
+ protected function __construct()
+ {
+ }
+
+ /**
+ * Gets all properties set on this element.
+ *
+ * @return array|Property[]
+ */
+ abstract public function getProperties();
+}
diff --git a/fusonic/opengraph/src/Elements/Image.php b/fusonic/opengraph/src/Elements/Image.php
new file mode 100644
index 00000000..831a0c4a
--- /dev/null
+++ b/fusonic/opengraph/src/Elements/Image.php
@@ -0,0 +1,100 @@
+<?php
+
+namespace Fusonic\OpenGraph\Elements;
+
+use Fusonic\OpenGraph\Property;
+
+/**
+ * An Open Graph image element.
+ */
+class Image extends ElementBase
+{
+ /**
+ * The URL of an image resource associated with the object.
+ *
+ * @var string
+ */
+ public $url;
+
+ /**
+ * An alternate URL to use if an image resource requires HTTPS.
+ *
+ * @var string
+ */
+ public $secureUrl;
+
+ /**
+ * The MIME type of an image resource.
+ *
+ * @var type
+ */
+ public $type;
+
+ /**
+ * The width of an image resource in pixels.
+ *
+ * @var int
+ */
+ public $width;
+
+ /**
+ * The height of an image resource in pixels.
+ *
+ * @var int
+ */
+ public $height;
+
+ /**
+ * Whether the image is user-generated or not.
+ *
+ * @var bool
+ */
+ public $userGenerated;
+
+ /**
+ * @param string $url URL to the image file.
+ */
+ public function __construct($url)
+ {
+ parent::__construct();
+
+ $this->url = $url;
+ }
+
+ /**
+ * Gets all properties set on this element.
+ *
+ * @return array|Property[]
+ */
+ public function getProperties()
+ {
+ $properties = [];
+
+ // URL must precede all other properties
+ if ($this->url !== null) {
+ $properties[] = new Property(Property::IMAGE_URL, $this->url);
+ }
+
+ if ($this->height !== null) {
+ $properties[] = new Property(Property::IMAGE_HEIGHT, $this->height);
+ }
+
+ if ($this->secureUrl !== null) {
+ $properties[] = new Property(Property::IMAGE_SECURE_URL, $this->secureUrl);
+ }
+
+ if ($this->type !== null) {
+ $properties[] = new Property(Property::IMAGE_TYPE, $this->type);
+ }
+
+ if ($this->width !== null) {
+ $properties[] = new Property(Property::IMAGE_WIDTH, $this->width);
+ }
+
+ if ($this->userGenerated !== null) {
+ $properties[] = new Property(Property::IMAGE_USER_GENERATED, $this->userGenerated);
+ }
+
+ return $properties;
+ }
+}
diff --git a/fusonic/opengraph/src/Elements/Video.php b/fusonic/opengraph/src/Elements/Video.php
new file mode 100644
index 00000000..ff5a3305
--- /dev/null
+++ b/fusonic/opengraph/src/Elements/Video.php
@@ -0,0 +1,89 @@
+<?php
+
+namespace Fusonic\OpenGraph\Elements;
+
+use Fusonic\OpenGraph\Property;
+
+/**
+ * An OpenGraph video element.
+ */
+class Video extends ElementBase
+{
+ /**
+ * The URL of a video resource associated with the object.
+ *
+ * @var string
+ */
+ public $url;
+
+ /**
+ * An alternate URL to use if a video resource requires HTTPS.
+ *
+ * @var string
+ */
+ public $secureUrl;
+
+ /**
+ * The MIME type of a video resource associated with the object.
+ *
+ * @var type
+ */
+ public $type;
+
+ /**
+ * The width of a video resource associated with the object in pixels.
+ *
+ * @var int
+ */
+ public $width;
+
+ /**
+ * The height of a video resource associated with the object in pixels.
+ *
+ * @var int
+ */
+ public $height;
+
+ /**
+ * @param string $url URL to the video.
+ */
+ public function __construct($url)
+ {
+ parent::__construct();
+
+ $this->url = $url;
+ }
+
+ /**
+ * Gets all properties set on this element.
+ *
+ * @return array|Property[]
+ */
+ public function getProperties()
+ {
+ $properties = [];
+
+ // URL must precede all other properties
+ if ($this->url !== null) {
+ $properties[] = new Property(Property::VIDEO_URL, $this->url);
+ }
+
+ if ($this->height !== null) {
+ $properties[] = new Property(Property::VIDEO_HEIGHT, $this->height);
+ }
+
+ if ($this->secureUrl !== null) {
+ $properties[] = new Property(Property::VIDEO_SECURE_URL, $this->secureUrl);
+ }
+
+ if ($this->type !== null) {
+ $properties[] = new Property(Property::VIDEO_TYPE, $this->type);
+ }
+
+ if ($this->width !== null) {
+ $properties[] = new Property(Property::VIDEO_WIDTH, $this->width);
+ }
+
+ return $properties;
+ }
+}
diff --git a/fusonic/opengraph/src/Objects/ObjectBase.php b/fusonic/opengraph/src/Objects/ObjectBase.php
new file mode 100644
index 00000000..33b9976d
--- /dev/null
+++ b/fusonic/opengraph/src/Objects/ObjectBase.php
@@ -0,0 +1,358 @@
+<?php
+
+namespace Fusonic\OpenGraph\Objects;
+
+use DateTimeImmutable;
+use Fusonic\OpenGraph\Elements\Audio;
+use Fusonic\OpenGraph\Elements\Image;
+use Fusonic\OpenGraph\Elements\Video;
+use Fusonic\OpenGraph\Property;
+use UnexpectedValueException;
+
+/**
+ * Abstract base class for all Open Graph objects (website, video, ...)
+ */
+abstract class ObjectBase
+{
+ /**
+ * An array of audio resources attached to the object.
+ *
+ * @var Audio[]
+ */
+ public array $audios = [];
+
+ /**
+ * A short description of the object.
+ */
+ public ?string $description = null;
+
+ /**
+ * The word that appears before the object's title in a sentence. This is an list of words from 'a', 'an', 'the',
+ * ' "" ', or 'auto'. If 'auto' is chosen, the consumer of the object will chose between 'a' or 'an'. The default is
+ * the blank, "".
+ */
+ public ?string $determiner = null;
+
+ /**
+ * An array of images attached to the object.
+ *
+ * @var Image[]
+ */
+ public array $images = [];
+
+ /**
+ * The locale that the object's tags are marked up in, in the format language_TERRITORY.
+ */
+ public ?string $locale = null;
+
+ /**
+ * An array of alternate locales in which the resource is available.
+ *
+ * @var string[]
+ */
+ public array $localeAlternate = [];
+
+ public ?bool $richAttachment = null;
+
+ /**
+ * An array of URLs of related resources.
+ *
+ * @var string[]
+ */
+ public array $seeAlso = [];
+
+ /**
+ * The name of the web site upon which the object resides.
+ */
+ public ?string $siteName = null;
+
+ /**
+ * The title of the object as it should appear in the graph.
+ */
+ public ?string $title = null;
+
+ /**
+ * The type of the object, such as 'article'.
+ */
+ public ?string $type = null;
+
+ /**
+ * The time when the object was last updated.
+ */
+ public ?DateTimeImmutable $updatedTime = null;
+
+ /**
+ * The canonical URL of the object, used as its ID in the graph.
+ */
+ public ?string $url = null;
+
+ /**
+ * An array of videos attached to the object.
+ *
+ * @var Video[]
+ */
+ public array $videos = [];
+
+ public function __construct()
+ {
+ }
+
+ /**
+ * Assigns all properties given to the this Object instance.
+ *
+ * @param array|Property[] $properties Array of all properties to assign.
+ * @param bool $debug Throw exceptions when parsing or not.
+ *
+ * @throws UnexpectedValueException
+ */
+ public function assignProperties(array $properties, $debug = false): void
+ {
+ foreach ($properties as $property) {
+ $name = $property->key;
+ $value = $property->value;
+
+ switch($name) {
+ case Property::AUDIO:
+ case Property::AUDIO_URL:
+ $this->audios[] = new Audio($value);
+ break;
+ case Property::AUDIO_SECURE_URL:
+ case Property::AUDIO_TYPE:
+ if (count($this->audios) > 0) {
+ $this->handleAudioAttribute($this->audios[count($this->audios) - 1], $name, $value);
+ } elseif ($debug) {
+ throw new UnexpectedValueException(
+ sprintf(
+ "Found '%s' property but no audio was found before.",
+ $name
+ )
+ );
+ }
+ break;
+ case Property::DESCRIPTION:
+ if ($this->description === null) {
+ $this->description = $value;
+ }
+ break;
+ case Property::DETERMINER:
+ if ($this->determiner === null) {
+ $this->determiner = $value;
+ }
+ break;
+ case Property::IMAGE:
+ case Property::IMAGE_URL:
+ $this->images[] = new Image($value);
+ break;
+ case Property::IMAGE_HEIGHT:
+ case Property::IMAGE_SECURE_URL:
+ case Property::IMAGE_TYPE:
+ case Property::IMAGE_WIDTH:
+ case Property::IMAGE_USER_GENERATED:
+ if (count($this->images) > 0) {
+ $this->handleImageAttribute($this->images[count($this->images) - 1], $name, $value);
+ } elseif ($debug) {
+ throw new UnexpectedValueException(
+ sprintf(
+ "Found '%s' property but no image was found before.",
+ $name
+ )
+ );
+ }
+ break;
+ case Property::LOCALE:
+ if ($this->locale === null) {
+ $this->locale = $value;
+ }
+ break;
+ case Property::LOCALE_ALTERNATE:
+ $this->localeAlternate[] = $value;
+ break;
+ case Property::RICH_ATTACHMENT:
+ $this->richAttachment = $this->convertToBoolean($value);
+ break;
+ case Property::SEE_ALSO:
+ $this->seeAlso[] = $value;
+ break;
+ case Property::SITE_NAME:
+ if ($this->siteName === null) {
+ $this->siteName = $value;
+ }
+ break;
+ case Property::TITLE:
+ if ($this->title === null) {
+ $this->title = $value;
+ }
+ break;
+ case Property::UPDATED_TIME:
+ if ($this->updatedTime === null) {
+ $this->updatedTime = $this->convertToDateTime($value);
+ }
+ break;
+ case Property::URL:
+ if ($this->url === null) {
+ $this->url = $value;
+ }
+ break;
+ case Property::VIDEO:
+ case Property::VIDEO_URL:
+ $this->videos[] = new Video($value);
+ break;
+ case Property::VIDEO_HEIGHT:
+ case Property::VIDEO_SECURE_URL:
+ case Property::VIDEO_TYPE:
+ case Property::VIDEO_WIDTH:
+ if (count($this->videos) > 0) {
+ $this->handleVideoAttribute($this->videos[count($this->videos) - 1], $name, $value);
+ } elseif ($debug) {
+ throw new UnexpectedValueException(sprintf(
+ "Found '%s' property but no video was found before.",
+ $name
+ ));
+ }
+ }
+ }
+ }
+
+ private function handleImageAttribute(Image $element, string $name, string $value): void
+ {
+ switch($name)
+ {
+ case Property::IMAGE_HEIGHT:
+ $element->height = (int)$value;
+ break;
+ case Property::IMAGE_WIDTH:
+ $element->width = (int)$value;
+ break;
+ case Property::IMAGE_TYPE:
+ $element->type = $value;
+ break;
+ case Property::IMAGE_SECURE_URL:
+ $element->secureUrl = $value;
+ break;
+ case Property::IMAGE_USER_GENERATED:
+ $element->userGenerated = $this->convertToBoolean($value);
+ break;
+ }
+ }
+
+ private function handleVideoAttribute(Video $element, string $name, string $value): void
+ {
+ switch($name)
+ {
+ case Property::VIDEO_HEIGHT:
+ $element->height = (int)$value;
+ break;
+ case Property::VIDEO_WIDTH:
+ $element->width = (int)$value;
+ break;
+ case Property::VIDEO_TYPE:
+ $element->type = $value;
+ break;
+ case Property::VIDEO_SECURE_URL:
+ $element->secureUrl = $value;
+ break;
+ }
+ }
+
+ private function handleAudioAttribute(Audio $element, string $name, string $value): void
+ {
+ switch($name)
+ {
+ case Property::AUDIO_TYPE:
+ $element->type = $value;
+ break;
+ case Property::AUDIO_SECURE_URL:
+ $element->secureUrl = $value;
+ break;
+ }
+ }
+
+ protected function convertToDateTime(string $value): ?DateTimeImmutable
+ {
+ try {
+ return new DateTimeImmutable($value);
+ } catch (\Exception $e) {
+ return null;
+ }
+ }
+
+ protected function convertToBoolean(string $value): bool
+ {
+ switch(strtolower($value))
+ {
+ case "1":
+ case "true":
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ /**
+ * Gets all properties set on this object.
+ *
+ * @return Property[]
+ */
+ public function getProperties(): array
+ {
+ $properties = [];
+
+ foreach ($this->audios as $audio) {
+ $properties = array_merge($properties, $audio->getProperties());
+ }
+
+ if ($this->title !== null) {
+ $properties[] = new Property(Property::TITLE, $this->title);
+ }
+
+ if ($this->description !== null) {
+ $properties[] = new Property(Property::DESCRIPTION, $this->description);
+ }
+
+ if ($this->determiner !== null) {
+ $properties[] = new Property(Property::DETERMINER, $this->determiner);
+ }
+
+ foreach ($this->images as $image) {
+ $properties = array_merge($properties, $image->getProperties());
+ }
+
+ if ($this->locale !== null) {
+ $properties[] = new Property(Property::LOCALE, $this->locale);
+ }
+
+ foreach ($this->localeAlternate as $locale) {
+ $properties[] = new Property(Property::LOCALE_ALTERNATE, $locale);
+ }
+
+ if ($this->richAttachment !== null) {
+ $properties[] = new Property(Property::RICH_ATTACHMENT, (int)$this->richAttachment);
+ }
+
+ foreach ($this->seeAlso as $seeAlso) {
+ $properties[] = new Property(Property::SEE_ALSO, $seeAlso);
+ }
+
+ if ($this->siteName !== null) {
+ $properties[] = new Property(Property::SITE_NAME, $this->siteName);
+ }
+
+ if ($this->type !== null) {
+ $properties[] = new Property(Property::TYPE, $this->type);
+ }
+
+ if ($this->updatedTime !== null) {
+ $properties[] = new Property(Property::UPDATED_TIME, $this->updatedTime->format("c"));
+ }
+
+ if ($this->url !== null) {
+ $properties[] = new Property(Property::URL, $this->url);
+ }
+
+ foreach ($this->videos as $video) {
+ $properties = array_merge($properties, $video->getProperties());
+ }
+
+ return $properties;
+ }
+}
diff --git a/fusonic/opengraph/src/Objects/Website.php b/fusonic/opengraph/src/Objects/Website.php
new file mode 100644
index 00000000..f66428cc
--- /dev/null
+++ b/fusonic/opengraph/src/Objects/Website.php
@@ -0,0 +1,21 @@
+<?php
+
+namespace Fusonic\OpenGraph\Objects;
+
+/**
+ * This object type represents a website. It is a simple object type and uses only common Open Graph properties. For
+ * specific pages within a website, the article object type should be used.
+ *
+ * https://developers.facebook.com/docs/reference/opengraph/object-type/website/
+ */
+class Website extends ObjectBase
+{
+ const TYPE = "website";
+
+ public function __construct()
+ {
+ parent::__construct();
+
+ $this->type = self::TYPE;
+ }
+}
diff --git a/fusonic/opengraph/src/Property.php b/fusonic/opengraph/src/Property.php
new file mode 100644
index 00000000..e94593bd
--- /dev/null
+++ b/fusonic/opengraph/src/Property.php
@@ -0,0 +1,54 @@
+<?php
+
+namespace Fusonic\OpenGraph;
+
+/**
+ * Class holding data for a single Open Graph property on a web page.
+ */
+class Property
+{
+ const AUDIO = "og:audio";
+ const AUDIO_SECURE_URL = "og:audio:secure_url";
+ const AUDIO_TYPE = "og:audio:type";
+ const AUDIO_URL = "og:audio:url";
+ const DESCRIPTION = "og:description";
+ const DETERMINER = "og:determiner";
+ const IMAGE = "og:image";
+ const IMAGE_HEIGHT = "og:image:height";
+ const IMAGE_SECURE_URL = "og:image:secure_url";
+ const IMAGE_TYPE = "og:image:type";
+ const IMAGE_URL = "og:image:url";
+ const IMAGE_WIDTH = "og:image:width";
+ const IMAGE_USER_GENERATED = "og:image:user_generated";
+ const LOCALE = "og:locale";
+ const LOCALE_ALTERNATE = "og:locale:alternate";
+ const RICH_ATTACHMENT = "og:rich_attachment";
+ const SEE_ALSO = "og:see_also";
+ const SITE_NAME = "og:site_name";
+ const TITLE = "og:title";
+ const TYPE = "og:type";
+ const UPDATED_TIME = "og:updated_time";
+ const URL = "og:url";
+ const VIDEO = "og:video";
+ const VIDEO_HEIGHT = "og:video:height";
+ const VIDEO_SECURE_URL = "og:video:secure_url";
+ const VIDEO_TYPE = "og:video:type";
+ const VIDEO_URL = "og:video:url";
+ const VIDEO_WIDTH = "og:video:width";
+
+ /**
+ * Key of the property without "og:" prefix.
+ */
+ public string $key;
+
+ /**
+ * Value of the property.
+ */
+ public $value;
+
+ public function __construct(string $key, $value)
+ {
+ $this->key = $key;
+ $this->value = $value;
+ }
+}
diff --git a/fusonic/opengraph/src/Publisher.php b/fusonic/opengraph/src/Publisher.php
new file mode 100644
index 00000000..059363ab
--- /dev/null
+++ b/fusonic/opengraph/src/Publisher.php
@@ -0,0 +1,65 @@
+<?php
+
+namespace Fusonic\OpenGraph;
+
+use DateTimeInterface;
+use Fusonic\OpenGraph\Objects\ObjectBase;
+use UnexpectedValueException;
+
+/**
+ * Class for generating Open Graph tags from objects.
+ */
+class Publisher
+{
+ const DOCTYPE_HTML5 = 1;
+ const DOCTYPE_XHTML = 2;
+
+ /**
+ * Defines the style in which HTML tags should be written. Use one of Publisher::DOCTYPE_HTML5 or
+ * Publisher::DOCTYPE_XHTML.
+ */
+ public int $doctype = self::DOCTYPE_HTML5;
+
+ public function __construct()
+ {
+ }
+
+ /**
+ * Generated HTML tags from the given object.
+ */
+ public function generateHtml(ObjectBase $object): string
+ {
+ $html = "";
+ $format = "<meta property=\"%s\" content=\"%s\"" . ($this->doctype == self::DOCTYPE_XHTML ? " />" : ">");
+
+ foreach ($object->getProperties() as $property) {
+ if ($html !== "") {
+ $html .= "\n";
+ }
+
+ if ($property->value === null) {
+ continue;
+ } elseif ($property->value instanceof DateTimeInterface) {
+ $value = $property->value->format("c");
+ } elseif (is_object($property->value)) {
+ throw new UnexpectedValueException(
+ sprintf(
+ "Cannot handle value of type '%s' for property '%s'.",
+ get_class($property->value),
+ $property->key
+ )
+ );
+ } elseif ($property->value === true) {
+ $value = "1";
+ } elseif ($property->value === false) {
+ $value = "0";
+ } else {
+ $value = (string)$property->value;
+ }
+
+ $html .= sprintf($format, $property->key, htmlspecialchars($value));
+ }
+
+ return $html;
+ }
+}
diff --git a/symfony/css-selector/CssSelectorConverter.php b/symfony/css-selector/CssSelectorConverter.php
new file mode 100644
index 00000000..bbb6afe2
--- /dev/null
+++ b/symfony/css-selector/CssSelectorConverter.php
@@ -0,0 +1,69 @@
+<?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;
+ private $cache;
+
+ private static $xmlCache = [];
+ private static $htmlCache = [];
+
+ /**
+ * @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->cache = &self::$htmlCache;
+ } else {
+ $this->cache = &self::$xmlCache;
+ }
+
+ $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.
+ *
+ * @return string
+ */
+ public function toXPath(string $cssExpr, string $prefix = 'descendant-or-self::')
+ {
+ return $this->cache[$prefix][$cssExpr] ?? $this->cache[$prefix][$cssExpr] = $this->translator->cssToXPath($cssExpr, $prefix);
+ }
+}
diff --git a/symfony/css-selector/Exception/ExceptionInterface.php b/symfony/css-selector/Exception/ExceptionInterface.php
new file mode 100644
index 00000000..9e259006
--- /dev/null
+++ b/symfony/css-selector/Exception/ExceptionInterface.php
@@ -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
+{
+}
diff --git a/symfony/css-selector/Exception/ExpressionErrorException.php b/symfony/css-selector/Exception/ExpressionErrorException.php
new file mode 100644
index 00000000..fd5deeab
--- /dev/null
+++ b/symfony/css-selector/Exception/ExpressionErrorException.php
@@ -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
+{
+}
diff --git a/symfony/css-selector/Exception/InternalErrorException.php b/symfony/css-selector/Exception/InternalErrorException.php
new file mode 100644
index 00000000..e60e5ed0
--- /dev/null
+++ b/symfony/css-selector/Exception/InternalErrorException.php
@@ -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
+{
+}
diff --git a/symfony/css-selector/Exception/ParseException.php b/symfony/css-selector/Exception/ParseException.php
new file mode 100644
index 00000000..3b0b0ee8
--- /dev/null
+++ b/symfony/css-selector/Exception/ParseException.php
@@ -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
+{
+}
diff --git a/symfony/css-selector/Exception/SyntaxErrorException.php b/symfony/css-selector/Exception/SyntaxErrorException.php
new file mode 100644
index 00000000..7deacf9c
--- /dev/null
+++ b/symfony/css-selector/Exception/SyntaxErrorException.php
@@ -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\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
+{
+ /**
+ * @return self
+ */
+ public static function unexpectedToken(string $expectedValue, Token $foundToken)
+ {
+ return new self(sprintf('Expected %s, but %s found.', $expectedValue, $foundToken));
+ }
+
+ /**
+ * @return self
+ */
+ public static function pseudoElementFound(string $pseudoElement, string $unexpectedLocation)
+ {
+ return new self(sprintf('Unexpected pseudo-element "::%s" found %s.', $pseudoElement, $unexpectedLocation));
+ }
+
+ /**
+ * @return self
+ */
+ public static function unclosedString(int $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.');
+ }
+}
diff --git a/symfony/css-selector/LICENSE b/symfony/css-selector/LICENSE
new file mode 100644
index 00000000..88bf75bb
--- /dev/null
+++ b/symfony/css-selector/LICENSE
@@ -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.
diff --git a/symfony/css-selector/Node/AbstractNode.php b/symfony/css-selector/Node/AbstractNode.php
new file mode 100644
index 00000000..1306aeac
--- /dev/null
+++ b/symfony/css-selector/Node/AbstractNode.php
@@ -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;
+ }
+}
diff --git a/symfony/css-selector/Node/AttributeNode.php b/symfony/css-selector/Node/AttributeNode.php
new file mode 100644
index 00000000..0b6e0ee0
--- /dev/null
+++ b/symfony/css-selector/Node/AttributeNode.php
@@ -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);
+ }
+}
diff --git a/symfony/css-selector/Node/ClassNode.php b/symfony/css-selector/Node/ClassNode.php
new file mode 100644
index 00000000..1efca808
--- /dev/null
+++ b/symfony/css-selector/Node/ClassNode.php
@@ -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);
+ }
+}
diff --git a/symfony/css-selector/Node/CombinedSelectorNode.php b/symfony/css-selector/Node/CombinedSelectorNode.php
new file mode 100644
index 00000000..a217a45e
--- /dev/null
+++ b/symfony/css-selector/Node/CombinedSelectorNode.php
@@ -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);
+ }
+}
diff --git a/symfony/css-selector/Node/ElementNode.php b/symfony/css-selector/Node/ElementNode.php
new file mode 100644
index 00000000..fbf8ea0f
--- /dev/null
+++ b/symfony/css-selector/Node/ElementNode.php
@@ -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);
+ }
+}
diff --git a/symfony/css-selector/Node/FunctionNode.php b/symfony/css-selector/Node/FunctionNode.php
new file mode 100644
index 00000000..c464cf7c
--- /dev/null
+++ b/symfony/css-selector/Node/FunctionNode.php
@@ -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.']' : '');
+ }
+}
diff --git a/symfony/css-selector/Node/HashNode.php b/symfony/css-selector/Node/HashNode.php
new file mode 100644
index 00000000..94114c09
--- /dev/null
+++ b/symfony/css-selector/Node/HashNode.php
@@ -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);
+ }
+}
diff --git a/symfony/css-selector/Node/NegationNode.php b/symfony/css-selector/Node/NegationNode.php
new file mode 100644
index 00000000..f00522fb
--- /dev/null
+++ b/symfony/css-selector/Node/NegationNode.php
@@ -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);
+ }
+}
diff --git a/symfony/css-selector/Node/NodeInterface.php b/symfony/css-selector/Node/NodeInterface.php
new file mode 100644
index 00000000..b078d26d
--- /dev/null
+++ b/symfony/css-selector/Node/NodeInterface.php
@@ -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;
+}
diff --git a/symfony/css-selector/Node/PseudoNode.php b/symfony/css-selector/Node/PseudoNode.php
new file mode 100644
index 00000000..12b7bd26
--- /dev/null
+++ b/symfony/css-selector/Node/PseudoNode.php
@@ -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);
+ }
+}
diff --git a/symfony/css-selector/Node/SelectorNode.php b/symfony/css-selector/Node/SelectorNode.php
new file mode 100644
index 00000000..6e52b2fa
--- /dev/null
+++ b/symfony/css-selector/Node/SelectorNode.php
@@ -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 : '');
+ }
+}
diff --git a/symfony/css-selector/Node/Specificity.php b/symfony/css-selector/Node/Specificity.php
new file mode 100644
index 00000000..b00f6d28
--- /dev/null
+++ b/symfony/css-selector/Node/Specificity.php
@@ -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;
+ }
+}
diff --git a/symfony/css-selector/Parser/Handler/CommentHandler.php b/symfony/css-selector/Parser/Handler/CommentHandler.php
new file mode 100644
index 00000000..93f31884
--- /dev/null
+++ b/symfony/css-selector/Parser/Handler/CommentHandler.php
@@ -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;
+ }
+}
diff --git a/symfony/css-selector/Parser/Handler/HandlerInterface.php b/symfony/css-selector/Parser/Handler/HandlerInterface.php
new file mode 100644
index 00000000..9ec714d5
--- /dev/null
+++ b/symfony/css-selector/Parser/Handler/HandlerInterface.php
@@ -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;
+}
diff --git a/symfony/css-selector/Parser/Handler/HashHandler.php b/symfony/css-selector/Parser/Handler/HashHandler.php
new file mode 100644
index 00000000..7ae9b438
--- /dev/null
+++ b/symfony/css-selector/Parser/Handler/HashHandler.php
@@ -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;
+ }
+}
diff --git a/symfony/css-selector/Parser/Handler/IdentifierHandler.php b/symfony/css-selector/Parser/Handler/IdentifierHandler.php
new file mode 100644
index 00000000..7b2a14e2
--- /dev/null
+++ b/symfony/css-selector/Parser/Handler/IdentifierHandler.php
@@ -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;
+ }
+}
diff --git a/symfony/css-selector/Parser/Handler/NumberHandler.php b/symfony/css-selector/Parser/Handler/NumberHandler.php
new file mode 100644
index 00000000..8291a68d
--- /dev/null
+++ b/symfony/css-selector/Parser/Handler/NumberHandler.php
@@ -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;
+ }
+}
diff --git a/symfony/css-selector/Parser/Handler/StringHandler.php b/symfony/css-selector/Parser/Handler/StringHandler.php
new file mode 100644
index 00000000..6ce83cdc
--- /dev/null
+++ b/symfony/css-selector/Parser/Handler/StringHandler.php
@@ -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;
+ }
+}
diff --git a/symfony/css-selector/Parser/Handler/WhitespaceHandler.php b/symfony/css-selector/Parser/Handler/WhitespaceHandler.php
new file mode 100644
index 00000000..21345e32
--- /dev/null
+++ b/symfony/css-selector/Parser/Handler/WhitespaceHandler.php
@@ -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;
+ }
+}
diff --git a/symfony/css-selector/Parser/Parser.php b/symfony/css-selector/Parser/Parser.php
new file mode 100644
index 00000000..d73489ed
--- /dev/null
+++ b/symfony/css-selector/Parser/Parser.php
@@ -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());
+ }
+}
diff --git a/symfony/css-selector/Parser/ParserInterface.php b/symfony/css-selector/Parser/ParserInterface.php
new file mode 100644
index 00000000..51c3d935
--- /dev/null
+++ b/symfony/css-selector/Parser/ParserInterface.php
@@ -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;
+}
diff --git a/symfony/css-selector/Parser/Reader.php b/symfony/css-selector/Parser/Reader.php
new file mode 100644
index 00000000..4b43effe
--- /dev/null
+++ b/symfony/css-selector/Parser/Reader.php
@@ -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;
+ }
+}
diff --git a/symfony/css-selector/Parser/Shortcut/ClassParser.php b/symfony/css-selector/Parser/Shortcut/ClassParser.php
new file mode 100644
index 00000000..17fa8c27
--- /dev/null
+++ b/symfony/css-selector/Parser/Shortcut/ClassParser.php
@@ -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 [];
+ }
+}
diff --git a/symfony/css-selector/Parser/Shortcut/ElementParser.php b/symfony/css-selector/Parser/Shortcut/ElementParser.php
new file mode 100644
index 00000000..8b9a8638
--- /dev/null
+++ b/symfony/css-selector/Parser/Shortcut/ElementParser.php
@@ -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 [];
+ }
+}
diff --git a/symfony/css-selector/Parser/Shortcut/EmptyStringParser.php b/symfony/css-selector/Parser/Shortcut/EmptyStringParser.php
new file mode 100644
index 00000000..222df5cd
--- /dev/null
+++ b/symfony/css-selector/Parser/Shortcut/EmptyStringParser.php
@@ -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 [];
+ }
+}
diff --git a/symfony/css-selector/Parser/Shortcut/HashParser.php b/symfony/css-selector/Parser/Shortcut/HashParser.php
new file mode 100644
index 00000000..fb07ee6c
--- /dev/null
+++ b/symfony/css-selector/Parser/Shortcut/HashParser.php
@@ -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 [];
+ }
+}
diff --git a/symfony/css-selector/Parser/Token.php b/symfony/css-selector/Parser/Token.php
new file mode 100644
index 00000000..a053203c
--- /dev/null
+++ b/symfony/css-selector/Parser/Token.php
@@ -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);
+ }
+}
diff --git a/symfony/css-selector/Parser/TokenStream.php b/symfony/css-selector/Parser/TokenStream.php
new file mode 100644
index 00000000..2085f2dd
--- /dev/null
+++ b/symfony/css-selector/Parser/TokenStream.php
@@ -0,0 +1,167 @@
+<?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 next identifier token.
+ *
+ * @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 next identifier or null if star delimiter token is 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();
+ }
+ }
+}
diff --git a/symfony/css-selector/Parser/Tokenizer/Tokenizer.php b/symfony/css-selector/Parser/Tokenizer/Tokenizer.php
new file mode 100644
index 00000000..e0dcc5bf
--- /dev/null
+++ b/symfony/css-selector/Parser/Tokenizer/Tokenizer.php
@@ -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();
+ }
+}
diff --git a/symfony/css-selector/Parser/Tokenizer/TokenizerEscaping.php b/symfony/css-selector/Parser/Tokenizer/TokenizerEscaping.php
new file mode 100644
index 00000000..013e827d
--- /dev/null
+++ b/symfony/css-selector/Parser/Tokenizer/TokenizerEscaping.php
@@ -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);
+ }
+}
diff --git a/symfony/css-selector/Parser/Tokenizer/TokenizerPatterns.php b/symfony/css-selector/Parser/Tokenizer/TokenizerPatterns.php
new file mode 100644
index 00000000..5f16ac48
--- /dev/null
+++ b/symfony/css-selector/Parser/Tokenizer/TokenizerPatterns.php
@@ -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';
+ }
+}
diff --git a/symfony/css-selector/README.md b/symfony/css-selector/README.md
new file mode 100644
index 00000000..ede4a3ac
--- /dev/null
+++ b/symfony/css-selector/README.md
@@ -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.
diff --git a/symfony/css-selector/XPath/Extension/AbstractExtension.php b/symfony/css-selector/XPath/Extension/AbstractExtension.php
new file mode 100644
index 00000000..44e0035a
--- /dev/null
+++ b/symfony/css-selector/XPath/Extension/AbstractExtension.php
@@ -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 [];
+ }
+}
diff --git a/symfony/css-selector/XPath/Extension/AttributeMatchingExtension.php b/symfony/css-selector/XPath/Extension/AttributeMatchingExtension.php
new file mode 100644
index 00000000..a9879f1b
--- /dev/null
+++ b/symfony/css-selector/XPath/Extension/AttributeMatchingExtension.php
@@ -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';
+ }
+}
diff --git a/symfony/css-selector/XPath/Extension/CombinationExtension.php b/symfony/css-selector/XPath/Extension/CombinationExtension.php
new file mode 100644
index 00000000..aee976e9
--- /dev/null
+++ b/symfony/css-selector/XPath/Extension/CombinationExtension.php
@@ -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';
+ }
+}
diff --git a/symfony/css-selector/XPath/Extension/ExtensionInterface.php b/symfony/css-selector/XPath/Extension/ExtensionInterface.php
new file mode 100644
index 00000000..1a74b90a
--- /dev/null
+++ b/symfony/css-selector/XPath/Extension/ExtensionInterface.php
@@ -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;
+}
diff --git a/symfony/css-selector/XPath/Extension/FunctionExtension.php b/symfony/css-selector/XPath/Extension/FunctionExtension.php
new file mode 100644
index 00000000..d3f7222a
--- /dev/null
+++ b/symfony/css-selector/XPath/Extension/FunctionExtension.php
@@ -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';
+ }
+}
diff --git a/symfony/css-selector/XPath/Extension/HtmlExtension.php b/symfony/css-selector/XPath/Extension/HtmlExtension.php
new file mode 100644
index 00000000..6edc0858
--- /dev/null
+++ b/symfony/css-selector/XPath/Extension/HtmlExtension.php
@@ -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';
+ }
+}
diff --git a/symfony/css-selector/XPath/Extension/NodeExtension.php b/symfony/css-selector/XPath/Extension/NodeExtension.php
new file mode 100644
index 00000000..aa6f3f70
--- /dev/null
+++ b/symfony/css-selector/XPath/Extension/NodeExtension.php
@@ -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);
+ }
+}
diff --git a/symfony/css-selector/XPath/Extension/PseudoClassExtension.php b/symfony/css-selector/XPath/Extension/PseudoClassExtension.php
new file mode 100644
index 00000000..a50b0486
--- /dev/null
+++ b/symfony/css-selector/XPath/Extension/PseudoClassExtension.php
@@ -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';
+ }
+}
diff --git a/symfony/css-selector/XPath/Translator.php b/symfony/css-selector/XPath/Translator.php
new file mode 100644
index 00000000..8ce47303
--- /dev/null
+++ b/symfony/css-selector/XPath/Translator.php
@@ -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, ?string $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);
+ }
+}
diff --git a/symfony/css-selector/XPath/TranslatorInterface.php b/symfony/css-selector/XPath/TranslatorInterface.php
new file mode 100644
index 00000000..c19eefb9
--- /dev/null
+++ b/symfony/css-selector/XPath/TranslatorInterface.php
@@ -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;
+}
diff --git a/symfony/css-selector/XPath/XPathExpr.php b/symfony/css-selector/XPath/XPathExpr.php
new file mode 100644
index 00000000..e45ce7d8
--- /dev/null
+++ b/symfony/css-selector/XPath/XPathExpr.php
@@ -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\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;
+ }
+
+ /**
+ * @return $this
+ */
+ 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;
+ }
+
+ /**
+ * @return $this
+ */
+ public function addNameTest(): self
+ {
+ if ('*' !== $this->element) {
+ $this->addCondition('name() = '.Translator::getXpathLiteral($this->element));
+ $this->element = '*';
+ }
+
+ return $this;
+ }
+
+ /**
+ * @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;
+ }
+}
diff --git a/symfony/css-selector/composer.json b/symfony/css-selector/composer.json
new file mode 100644
index 00000000..f0b71249
--- /dev/null
+++ b/symfony/css-selector/composer.json
@@ -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.2.5",
+ "symfony/polyfill-php80": "^1.16"
+ },
+ "autoload": {
+ "psr-4": { "Symfony\\Component\\CssSelector\\": "" },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "minimum-stability": "dev"
+}
diff --git a/symfony/dom-crawler/AbstractUriElement.php b/symfony/dom-crawler/AbstractUriElement.php
new file mode 100644
index 00000000..8ff0b992
--- /dev/null
+++ b/symfony/dom-crawler/AbstractUriElement.php
@@ -0,0 +1,131 @@
+<?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\DomCrawler;
+
+/**
+ * Any HTML element that can link to an URI.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ */
+abstract class AbstractUriElement
+{
+ /**
+ * @var \DOMElement
+ */
+ protected $node;
+
+ /**
+ * @var string|null The method to use for the element
+ */
+ protected $method;
+
+ /**
+ * @var string The URI of the page where the element is embedded (or the base href)
+ */
+ protected $currentUri;
+
+ /**
+ * @param \DOMElement $node A \DOMElement instance
+ * @param string|null $currentUri The URI of the page where the link is embedded (or the base href)
+ * @param string|null $method The method to use for the link (GET by default)
+ *
+ * @throws \InvalidArgumentException if the node is not a link
+ */
+ public function __construct(\DOMElement $node, string $currentUri = null, ?string $method = 'GET')
+ {
+ $this->setNode($node);
+ $this->method = $method ? strtoupper($method) : null;
+ $this->currentUri = $currentUri;
+
+ $elementUriIsRelative = null === parse_url(trim($this->getRawUri()), \PHP_URL_SCHEME);
+ $baseUriIsAbsolute = null !== $this->currentUri && \in_array(strtolower(substr($this->currentUri, 0, 4)), ['http', 'file']);
+ if ($elementUriIsRelative && !$baseUriIsAbsolute) {
+ throw new \InvalidArgumentException(sprintf('The URL of the element is relative, so you must define its base URI passing an absolute URL to the constructor of the "%s" class ("%s" was passed).', __CLASS__, $this->currentUri));
+ }
+ }
+
+ /**
+ * Gets the node associated with this link.
+ *
+ * @return \DOMElement
+ */
+ public function getNode()
+ {
+ return $this->node;
+ }
+
+ /**
+ * Gets the method associated with this link.
+ *
+ * @return string
+ */
+ public function getMethod()
+ {
+ return $this->method ?? 'GET';
+ }
+
+ /**
+ * Gets the URI associated with this link.
+ *
+ * @return string
+ */
+ public function getUri()
+ {
+ return UriResolver::resolve($this->getRawUri(), $this->currentUri);
+ }
+
+ /**
+ * Returns raw URI data.
+ *
+ * @return string
+ */
+ abstract protected function getRawUri();
+
+ /**
+ * Returns the canonicalized URI path (see RFC 3986, section 5.2.4).
+ *
+ * @param string $path URI path
+ *
+ * @return string
+ */
+ protected function canonicalizePath(string $path)
+ {
+ if ('' === $path || '/' === $path) {
+ return $path;
+ }
+
+ if (str_ends_with($path, '.')) {
+ $path .= '/';
+ }
+
+ $output = [];
+
+ foreach (explode('/', $path) as $segment) {
+ if ('..' === $segment) {
+ array_pop($output);
+ } elseif ('.' !== $segment) {
+ $output[] = $segment;
+ }
+ }
+
+ return implode('/', $output);
+ }
+
+ /**
+ * Sets current \DOMElement instance.
+ *
+ * @param \DOMElement $node A \DOMElement instance
+ *
+ * @throws \LogicException If given node is not an anchor
+ */
+ abstract protected function setNode(\DOMElement $node);
+}
diff --git a/symfony/dom-crawler/Crawler.php b/symfony/dom-crawler/Crawler.php
new file mode 100644
index 00000000..a32bc1dd
--- /dev/null
+++ b/symfony/dom-crawler/Crawler.php
@@ -0,0 +1,1309 @@
+<?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\DomCrawler;
+
+use Masterminds\HTML5;
+use Symfony\Component\CssSelector\CssSelectorConverter;
+
+/**
+ * Crawler eases navigation of a list of \DOMNode objects.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ *
+ * @implements \IteratorAggregate<int, \DOMNode>
+ */
+class Crawler implements \Countable, \IteratorAggregate
+{
+ /**
+ * @var string|null
+ */
+ protected $uri;
+
+ /**
+ * The default namespace prefix to be used with XPath and CSS expressions.
+ *
+ * @var string
+ */
+ private $defaultNamespacePrefix = 'default';
+
+ /**
+ * A map of manually registered namespaces.
+ *
+ * @var array<string, string>
+ */
+ private $namespaces = [];
+
+ /**
+ * A map of cached namespaces.
+ *
+ * @var \ArrayObject
+ */
+ private $cachedNamespaces;
+
+ /**
+ * The base href value.
+ *
+ * @var string|null
+ */
+ private $baseHref;
+
+ /**
+ * @var \DOMDocument|null
+ */
+ private $document;
+
+ /**
+ * @var list<\DOMNode>
+ */
+ private $nodes = [];
+
+ /**
+ * Whether the Crawler contains HTML or XML content (used when converting CSS to XPath).
+ *
+ * @var bool
+ */
+ private $isHtml = true;
+
+ /**
+ * @var HTML5|null
+ */
+ private $html5Parser;
+
+ /**
+ * @param \DOMNodeList|\DOMNode|\DOMNode[]|string|null $node A Node to use as the base for the crawling
+ */
+ public function __construct($node = null, string $uri = null, string $baseHref = null)
+ {
+ $this->uri = $uri;
+ $this->baseHref = $baseHref ?: $uri;
+ $this->html5Parser = class_exists(HTML5::class) ? new HTML5(['disable_html_ns' => true]) : null;
+ $this->cachedNamespaces = new \ArrayObject();
+
+ $this->add($node);
+ }
+
+ /**
+ * Returns the current URI.
+ *
+ * @return string|null
+ */
+ public function getUri()
+ {
+ return $this->uri;
+ }
+
+ /**
+ * Returns base href.
+ *
+ * @return string|null
+ */
+ public function getBaseHref()
+ {
+ return $this->baseHref;
+ }
+
+ /**
+ * Removes all the nodes.
+ */
+ public function clear()
+ {
+ $this->nodes = [];
+ $this->document = null;
+ $this->cachedNamespaces = new \ArrayObject();
+ }
+
+ /**
+ * Adds a node to the current list of nodes.
+ *
+ * This method uses the appropriate specialized add*() method based
+ * on the type of the argument.
+ *
+ * @param \DOMNodeList|\DOMNode|\DOMNode[]|string|null $node A node
+ *
+ * @throws \InvalidArgumentException when node is not the expected type
+ */
+ public function add($node)
+ {
+ if ($node instanceof \DOMNodeList) {
+ $this->addNodeList($node);
+ } elseif ($node instanceof \DOMNode) {
+ $this->addNode($node);
+ } elseif (\is_array($node)) {
+ $this->addNodes($node);
+ } elseif (\is_string($node)) {
+ $this->addContent($node);
+ } elseif (null !== $node) {
+ throw new \InvalidArgumentException(sprintf('Expecting a DOMNodeList or DOMNode instance, an array, a string, or null, but got "%s".', get_debug_type($node)));
+ }
+ }
+
+ /**
+ * Adds HTML/XML content.
+ *
+ * If the charset is not set via the content type, it is assumed to be UTF-8,
+ * or ISO-8859-1 as a fallback, which is the default charset defined by the
+ * HTTP 1.1 specification.
+ */
+ public function addContent(string $content, string $type = null)
+ {
+ if (empty($type)) {
+ $type = str_starts_with($content, '<?xml') ? 'application/xml' : 'text/html';
+ }
+
+ // DOM only for HTML/XML content
+ if (!preg_match('/(x|ht)ml/i', $type, $xmlMatches)) {
+ return;
+ }
+
+ $charset = preg_match('//u', $content) ? 'UTF-8' : 'ISO-8859-1';
+
+ // http://www.w3.org/TR/encoding/#encodings
+ // http://www.w3.org/TR/REC-xml/#NT-EncName
+ $content = preg_replace_callback('/(charset *= *["\']?)([a-zA-Z\-0-9_:.]+)/i', function ($m) use (&$charset) {
+ if ('charset=' === $this->convertToHtmlEntities('charset=', $m[2])) {
+ $charset = $m[2];
+ }
+
+ return $m[1].$charset;
+ }, $content, 1);
+
+ if ('x' === $xmlMatches[1]) {
+ $this->addXmlContent($content, $charset);
+ } else {
+ $this->addHtmlContent($content, $charset);
+ }
+ }
+
+ /**
+ * Adds an HTML content to the list of nodes.
+ *
+ * The libxml errors are disabled when the content is parsed.
+ *
+ * If you want to get parsing errors, be sure to enable
+ * internal errors via libxml_use_internal_errors(true)
+ * and then, get the errors via libxml_get_errors(). Be
+ * sure to clear errors with libxml_clear_errors() afterward.
+ */
+ public function addHtmlContent(string $content, string $charset = 'UTF-8')
+ {
+ $dom = $this->parseHtmlString($content, $charset);
+ $this->addDocument($dom);
+
+ $base = $this->filterRelativeXPath('descendant-or-self::base')->extract(['href']);
+
+ $baseHref = current($base);
+ if (\count($base) && !empty($baseHref)) {
+ if ($this->baseHref) {
+ $linkNode = $dom->createElement('a');
+ $linkNode->setAttribute('href', $baseHref);
+ $link = new Link($linkNode, $this->baseHref);
+ $this->baseHref = $link->getUri();
+ } else {
+ $this->baseHref = $baseHref;
+ }
+ }
+ }
+
+ /**
+ * Adds an XML content to the list of nodes.
+ *
+ * The libxml errors are disabled when the content is parsed.
+ *
+ * If you want to get parsing errors, be sure to enable
+ * internal errors via libxml_use_internal_errors(true)
+ * and then, get the errors via libxml_get_errors(). Be
+ * sure to clear errors with libxml_clear_errors() afterward.
+ *
+ * @param int $options Bitwise OR of the libxml option constants
+ * LIBXML_PARSEHUGE is dangerous, see
+ * http://symfony.com/blog/security-release-symfony-2-0-17-released
+ */
+ public function addXmlContent(string $content, string $charset = 'UTF-8', int $options = \LIBXML_NONET)
+ {
+ // remove the default namespace if it's the only namespace to make XPath expressions simpler
+ if (!preg_match('/xmlns:/', $content)) {
+ $content = str_replace('xmlns', 'ns', $content);
+ }
+
+ $internalErrors = libxml_use_internal_errors(true);
+ if (\LIBXML_VERSION < 20900) {
+ $disableEntities = libxml_disable_entity_loader(true);
+ }
+
+ $dom = new \DOMDocument('1.0', $charset);
+ $dom->validateOnParse = true;
+
+ if ('' !== trim($content)) {
+ @$dom->loadXML($content, $options);
+ }
+
+ libxml_use_internal_errors($internalErrors);
+ if (\LIBXML_VERSION < 20900) {
+ libxml_disable_entity_loader($disableEntities);
+ }
+
+ $this->addDocument($dom);
+
+ $this->isHtml = false;
+ }
+
+ /**
+ * Adds a \DOMDocument to the list of nodes.
+ *
+ * @param \DOMDocument $dom A \DOMDocument instance
+ */
+ public function addDocument(\DOMDocument $dom)
+ {
+ if ($dom->documentElement) {
+ $this->addNode($dom->documentElement);
+ }
+ }
+
+ /**
+ * Adds a \DOMNodeList to the list of nodes.
+ *
+ * @param \DOMNodeList $nodes A \DOMNodeList instance
+ */
+ public function addNodeList(\DOMNodeList $nodes)
+ {
+ foreach ($nodes as $node) {
+ if ($node instanceof \DOMNode) {
+ $this->addNode($node);
+ }
+ }
+ }
+
+ /**
+ * Adds an array of \DOMNode instances to the list of nodes.
+ *
+ * @param \DOMNode[] $nodes An array of \DOMNode instances
+ */
+ public function addNodes(array $nodes)
+ {
+ foreach ($nodes as $node) {
+ $this->add($node);
+ }
+ }
+
+ /**
+ * Adds a \DOMNode instance to the list of nodes.
+ *
+ * @param \DOMNode $node A \DOMNode instance
+ */
+ public function addNode(\DOMNode $node)
+ {
+ if ($node instanceof \DOMDocument) {
+ $node = $node->documentElement;
+ }
+
+ if (null !== $this->document && $this->document !== $node->ownerDocument) {
+ throw new \InvalidArgumentException('Attaching DOM nodes from multiple documents in the same crawler is forbidden.');
+ }
+
+ if (null === $this->document) {
+ $this->document = $node->ownerDocument;
+ }
+
+ // Don't add duplicate nodes in the Crawler
+ if (\in_array($node, $this->nodes, true)) {
+ return;
+ }
+
+ $this->nodes[] = $node;
+ }
+
+ /**
+ * Returns a node given its position in the node list.
+ *
+ * @return static
+ */
+ public function eq(int $position)
+ {
+ if (isset($this->nodes[$position])) {
+ return $this->createSubCrawler($this->nodes[$position]);
+ }
+
+ return $this->createSubCrawler(null);
+ }
+
+ /**
+ * Calls an anonymous function on each node of the list.
+ *
+ * The anonymous function receives the position and the node wrapped
+ * in a Crawler instance as arguments.
+ *
+ * Example:
+ *
+ * $crawler->filter('h1')->each(function ($node, $i) {
+ * return $node->text();
+ * });
+ *
+ * @param \Closure $closure An anonymous function
+ *
+ * @return array An array of values returned by the anonymous function
+ */
+ public function each(\Closure $closure)
+ {
+ $data = [];
+ foreach ($this->nodes as $i => $node) {
+ $data[] = $closure($this->createSubCrawler($node), $i);
+ }
+
+ return $data;
+ }
+
+ /**
+ * Slices the list of nodes by $offset and $length.
+ *
+ * @return static
+ */
+ public function slice(int $offset = 0, int $length = null)
+ {
+ return $this->createSubCrawler(\array_slice($this->nodes, $offset, $length));
+ }
+
+ /**
+ * Reduces the list of nodes by calling an anonymous function.
+ *
+ * To remove a node from the list, the anonymous function must return false.
+ *
+ * @param \Closure $closure An anonymous function
+ *
+ * @return static
+ */
+ public function reduce(\Closure $closure)
+ {
+ $nodes = [];
+ foreach ($this->nodes as $i => $node) {
+ if (false !== $closure($this->createSubCrawler($node), $i)) {
+ $nodes[] = $node;
+ }
+ }
+
+ return $this->createSubCrawler($nodes);
+ }
+
+ /**
+ * Returns the first node of the current selection.
+ *
+ * @return static
+ */
+ public function first()
+ {
+ return $this->eq(0);
+ }
+
+ /**
+ * Returns the last node of the current selection.
+ *
+ * @return static
+ */
+ public function last()
+ {
+ return $this->eq(\count($this->nodes) - 1);
+ }
+
+ /**
+ * Returns the siblings nodes of the current selection.
+ *
+ * @return static
+ *
+ * @throws \InvalidArgumentException When current node is empty
+ */
+ public function siblings()
+ {
+ if (!$this->nodes) {
+ throw new \InvalidArgumentException('The current node list is empty.');
+ }
+
+ return $this->createSubCrawler($this->sibling($this->getNode(0)->parentNode->firstChild));
+ }
+
+ public function matches(string $selector): bool
+ {
+ if (!$this->nodes) {
+ return false;
+ }
+
+ $converter = $this->createCssSelectorConverter();
+ $xpath = $converter->toXPath($selector, 'self::');
+
+ return 0 !== $this->filterRelativeXPath($xpath)->count();
+ }
+
+ /**
+ * Return first parents (heading toward the document root) of the Element that matches the provided selector.
+ *
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/Element/closest#Polyfill
+ *
+ * @throws \InvalidArgumentException When current node is empty
+ */
+ public function closest(string $selector): ?self
+ {
+ if (!$this->nodes) {
+ throw new \InvalidArgumentException('The current node list is empty.');
+ }
+
+ $domNode = $this->getNode(0);
+
+ while (\XML_ELEMENT_NODE === $domNode->nodeType) {
+ $node = $this->createSubCrawler($domNode);
+ if ($node->matches($selector)) {
+ return $node;
+ }
+
+ $domNode = $node->getNode(0)->parentNode;
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns the next siblings nodes of the current selection.
+ *
+ * @return static
+ *
+ * @throws \InvalidArgumentException When current node is empty
+ */
+ public function nextAll()
+ {
+ if (!$this->nodes) {
+ throw new \InvalidArgumentException('The current node list is empty.');
+ }
+
+ return $this->createSubCrawler($this->sibling($this->getNode(0)));
+ }
+
+ /**
+ * Returns the previous sibling nodes of the current selection.
+ *
+ * @return static
+ *
+ * @throws \InvalidArgumentException
+ */
+ public function previousAll()
+ {
+ if (!$this->nodes) {
+ throw new \InvalidArgumentException('The current node list is empty.');
+ }
+
+ return $this->createSubCrawler($this->sibling($this->getNode(0), 'previousSibling'));
+ }
+
+ /**
+ * Returns the parent nodes of the current selection.
+ *
+ * @return static
+ *
+ * @throws \InvalidArgumentException When current node is empty
+ */
+ public function parents()
+ {
+ trigger_deprecation('symfony/dom-crawler', '5.3', 'The %s() method is deprecated, use ancestors() instead.', __METHOD__);
+
+ return $this->ancestors();
+ }
+
+ /**
+ * Returns the ancestors of the current selection.
+ *
+ * @return static
+ *
+ * @throws \InvalidArgumentException When the current node is empty
+ */
+ public function ancestors()
+ {
+ if (!$this->nodes) {
+ throw new \InvalidArgumentException('The current node list is empty.');
+ }
+
+ $node = $this->getNode(0);
+ $nodes = [];
+
+ while ($node = $node->parentNode) {
+ if (\XML_ELEMENT_NODE === $node->nodeType) {
+ $nodes[] = $node;
+ }
+ }
+
+ return $this->createSubCrawler($nodes);
+ }
+
+ /**
+ * Returns the children nodes of the current selection.
+ *
+ * @return static
+ *
+ * @throws \InvalidArgumentException When current node is empty
+ * @throws \RuntimeException If the CssSelector Component is not available and $selector is provided
+ */
+ public function children(string $selector = null)
+ {
+ if (!$this->nodes) {
+ throw new \InvalidArgumentException('The current node list is empty.');
+ }
+
+ if (null !== $selector) {
+ $converter = $this->createCssSelectorConverter();
+ $xpath = $converter->toXPath($selector, 'child::');
+
+ return $this->filterRelativeXPath($xpath);
+ }
+
+ $node = $this->getNode(0)->firstChild;
+
+ return $this->createSubCrawler($node ? $this->sibling($node) : []);
+ }
+
+ /**
+ * Returns the attribute value of the first node of the list.
+ *
+ * @return string|null
+ *
+ * @throws \InvalidArgumentException When current node is empty
+ */
+ public function attr(string $attribute)
+ {
+ if (!$this->nodes) {
+ throw new \InvalidArgumentException('The current node list is empty.');
+ }
+
+ $node = $this->getNode(0);
+
+ return $node->hasAttribute($attribute) ? $node->getAttribute($attribute) : null;
+ }
+
+ /**
+ * Returns the node name of the first node of the list.
+ *
+ * @return string
+ *
+ * @throws \InvalidArgumentException When current node is empty
+ */
+ public function nodeName()
+ {
+ if (!$this->nodes) {
+ throw new \InvalidArgumentException('The current node list is empty.');
+ }
+
+ return $this->getNode(0)->nodeName;
+ }
+
+ /**
+ * Returns the text of the first node of the list.
+ *
+ * Pass true as the second argument to normalize whitespaces.
+ *
+ * @param string|null $default When not null: the value to return when the current node is empty
+ * @param bool $normalizeWhitespace Whether whitespaces should be trimmed and normalized to single spaces
+ *
+ * @return string
+ *
+ * @throws \InvalidArgumentException When current node is empty
+ */
+ public function text(string $default = null, bool $normalizeWhitespace = true)
+ {
+ if (!$this->nodes) {
+ if (null !== $default) {
+ return $default;
+ }
+
+ throw new \InvalidArgumentException('The current node list is empty.');
+ }
+
+ $text = $this->getNode(0)->nodeValue;
+
+ if ($normalizeWhitespace) {
+ return trim(preg_replace('/(?:\s{2,}+|[^\S ])/', ' ', $text));
+ }
+
+ return $text;
+ }
+
+ /**
+ * Returns only the inner text that is the direct descendent of the current node, excluding any child nodes.
+ */
+ public function innerText(): string
+ {
+ return $this->filterXPath('.//text()')->text();
+ }
+
+ /**
+ * Returns the first node of the list as HTML.
+ *
+ * @param string|null $default When not null: the value to return when the current node is empty
+ *
+ * @return string
+ *
+ * @throws \InvalidArgumentException When current node is empty
+ */
+ public function html(string $default = null)
+ {
+ if (!$this->nodes) {
+ if (null !== $default) {
+ return $default;
+ }
+
+ throw new \InvalidArgumentException('The current node list is empty.');
+ }
+
+ $node = $this->getNode(0);
+ $owner = $node->ownerDocument;
+
+ if (null !== $this->html5Parser && '<!DOCTYPE html>' === $owner->saveXML($owner->childNodes[0])) {
+ $owner = $this->html5Parser;
+ }
+
+ $html = '';
+ foreach ($node->childNodes as $child) {
+ $html .= $owner->saveHTML($child);
+ }
+
+ return $html;
+ }
+
+ public function outerHtml(): string
+ {
+ if (!\count($this)) {
+ throw new \InvalidArgumentException('The current node list is empty.');
+ }
+
+ $node = $this->getNode(0);
+ $owner = $node->ownerDocument;
+
+ if (null !== $this->html5Parser && '<!DOCTYPE html>' === $owner->saveXML($owner->childNodes[0])) {
+ $owner = $this->html5Parser;
+ }
+
+ return $owner->saveHTML($node);
+ }
+
+ /**
+ * Evaluates an XPath expression.
+ *
+ * Since an XPath expression might evaluate to either a simple type or a \DOMNodeList,
+ * this method will return either an array of simple types or a new Crawler instance.
+ *
+ * @return array|Crawler
+ */
+ public function evaluate(string $xpath)
+ {
+ if (null === $this->document) {
+ throw new \LogicException('Cannot evaluate the expression on an uninitialized crawler.');
+ }
+
+ $data = [];
+ $domxpath = $this->createDOMXPath($this->document, $this->findNamespacePrefixes($xpath));
+
+ foreach ($this->nodes as $node) {
+ $data[] = $domxpath->evaluate($xpath, $node);
+ }
+
+ if (isset($data[0]) && $data[0] instanceof \DOMNodeList) {
+ return $this->createSubCrawler($data);
+ }
+
+ return $data;
+ }
+
+ /**
+ * Extracts information from the list of nodes.
+ *
+ * You can extract attributes or/and the node value (_text).
+ *
+ * Example:
+ *
+ * $crawler->filter('h1 a')->extract(['_text', 'href']);
+ *
+ * @return array
+ */
+ public function extract(array $attributes)
+ {
+ $count = \count($attributes);
+
+ $data = [];
+ foreach ($this->nodes as $node) {
+ $elements = [];
+ foreach ($attributes as $attribute) {
+ if ('_text' === $attribute) {
+ $elements[] = $node->nodeValue;
+ } elseif ('_name' === $attribute) {
+ $elements[] = $node->nodeName;
+ } else {
+ $elements[] = $node->getAttribute($attribute);
+ }
+ }
+
+ $data[] = 1 === $count ? $elements[0] : $elements;
+ }
+
+ return $data;
+ }
+
+ /**
+ * Filters the list of nodes with an XPath expression.
+ *
+ * The XPath expression is evaluated in the context of the crawler, which
+ * is considered as a fake parent of the elements inside it.
+ * This means that a child selector "div" or "./div" will match only
+ * the div elements of the current crawler, not their children.
+ *
+ * @return static
+ */
+ public function filterXPath(string $xpath)
+ {
+ $xpath = $this->relativize($xpath);
+
+ // If we dropped all expressions in the XPath while preparing it, there would be no match
+ if ('' === $xpath) {
+ return $this->createSubCrawler(null);
+ }
+
+ return $this->filterRelativeXPath($xpath);
+ }
+
+ /**
+ * Filters the list of nodes with a CSS selector.
+ *
+ * This method only works if you have installed the CssSelector Symfony Component.
+ *
+ * @return static
+ *
+ * @throws \RuntimeException if the CssSelector Component is not available
+ */
+ public function filter(string $selector)
+ {
+ $converter = $this->createCssSelectorConverter();
+
+ // The CssSelector already prefixes the selector with descendant-or-self::
+ return $this->filterRelativeXPath($converter->toXPath($selector));
+ }
+
+ /**
+ * Selects links by name or alt value for clickable images.
+ *
+ * @return static
+ */
+ public function selectLink(string $value)
+ {
+ return $this->filterRelativeXPath(
+ sprintf('descendant-or-self::a[contains(concat(\' \', normalize-space(string(.)), \' \'), %1$s) or ./img[contains(concat(\' \', normalize-space(string(@alt)), \' \'), %1$s)]]', static::xpathLiteral(' '.$value.' '))
+ );
+ }
+
+ /**
+ * Selects images by alt value.
+ *
+ * @return static
+ */
+ public function selectImage(string $value)
+ {
+ $xpath = sprintf('descendant-or-self::img[contains(normalize-space(string(@alt)), %s)]', static::xpathLiteral($value));
+
+ return $this->filterRelativeXPath($xpath);
+ }
+
+ /**
+ * Selects a button by name or alt value for images.
+ *
+ * @return static
+ */
+ public function selectButton(string $value)
+ {
+ return $this->filterRelativeXPath(
+ sprintf('descendant-or-self::input[((contains(%1$s, "submit") or contains(%1$s, "button")) and contains(concat(\' \', normalize-space(string(@value)), \' \'), %2$s)) or (contains(%1$s, "image") and contains(concat(\' \', normalize-space(string(@alt)), \' \'), %2$s)) or @id=%3$s or @name=%3$s] | descendant-or-self::button[contains(concat(\' \', normalize-space(string(.)), \' \'), %2$s) or @id=%3$s or @name=%3$s]', 'translate(@type, "ABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyz")', static::xpathLiteral(' '.$value.' '), static::xpathLiteral($value))
+ );
+ }
+
+ /**
+ * Returns a Link object for the first node in the list.
+ *
+ * @return Link
+ *
+ * @throws \InvalidArgumentException If the current node list is empty or the selected node is not instance of DOMElement
+ */
+ public function link(string $method = 'get')
+ {
+ if (!$this->nodes) {
+ throw new \InvalidArgumentException('The current node list is empty.');
+ }
+
+ $node = $this->getNode(0);
+
+ if (!$node instanceof \DOMElement) {
+ throw new \InvalidArgumentException(sprintf('The selected node should be instance of DOMElement, got "%s".', get_debug_type($node)));
+ }
+
+ return new Link($node, $this->baseHref, $method);
+ }
+
+ /**
+ * Returns an array of Link objects for the nodes in the list.
+ *
+ * @return Link[]
+ *
+ * @throws \InvalidArgumentException If the current node list contains non-DOMElement instances
+ */
+ public function links()
+ {
+ $links = [];
+ foreach ($this->nodes as $node) {
+ if (!$node instanceof \DOMElement) {
+ throw new \InvalidArgumentException(sprintf('The current node list should contain only DOMElement instances, "%s" found.', get_debug_type($node)));
+ }
+
+ $links[] = new Link($node, $this->baseHref, 'get');
+ }
+
+ return $links;
+ }
+
+ /**
+ * Returns an Image object for the first node in the list.
+ *
+ * @return Image
+ *
+ * @throws \InvalidArgumentException If the current node list is empty
+ */
+ public function image()
+ {
+ if (!\count($this)) {
+ throw new \InvalidArgumentException('The current node list is empty.');
+ }
+
+ $node = $this->getNode(0);
+
+ if (!$node instanceof \DOMElement) {
+ throw new \InvalidArgumentException(sprintf('The selected node should be instance of DOMElement, got "%s".', get_debug_type($node)));
+ }
+
+ return new Image($node, $this->baseHref);
+ }
+
+ /**
+ * Returns an array of Image objects for the nodes in the list.
+ *
+ * @return Image[]
+ */
+ public function images()
+ {
+ $images = [];
+ foreach ($this as $node) {
+ if (!$node instanceof \DOMElement) {
+ throw new \InvalidArgumentException(sprintf('The current node list should contain only DOMElement instances, "%s" found.', get_debug_type($node)));
+ }
+
+ $images[] = new Image($node, $this->baseHref);
+ }
+
+ return $images;
+ }
+
+ /**
+ * Returns a Form object for the first node in the list.
+ *
+ * @return Form
+ *
+ * @throws \InvalidArgumentException If the current node list is empty or the selected node is not instance of DOMElement
+ */
+ public function form(array $values = null, string $method = null)
+ {
+ if (!$this->nodes) {
+ throw new \InvalidArgumentException('The current node list is empty.');
+ }
+
+ $node = $this->getNode(0);
+
+ if (!$node instanceof \DOMElement) {
+ throw new \InvalidArgumentException(sprintf('The selected node should be instance of DOMElement, got "%s".', get_debug_type($node)));
+ }
+
+ $form = new Form($node, $this->uri, $method, $this->baseHref);
+
+ if (null !== $values) {
+ $form->setValues($values);
+ }
+
+ return $form;
+ }
+
+ /**
+ * Overloads a default namespace prefix to be used with XPath and CSS expressions.
+ */
+ public function setDefaultNamespacePrefix(string $prefix)
+ {
+ $this->defaultNamespacePrefix = $prefix;
+ }
+
+ public function registerNamespace(string $prefix, string $namespace)
+ {
+ $this->namespaces[$prefix] = $namespace;
+ }
+
+ /**
+ * Converts string for XPath expressions.
+ *
+ * Escaped characters are: quotes (") and apostrophe (').
+ *
+ * Examples:
+ *
+ * echo Crawler::xpathLiteral('foo " bar');
+ * //prints 'foo " bar'
+ *
+ * echo Crawler::xpathLiteral("foo ' bar");
+ * //prints "foo ' bar"
+ *
+ * echo Crawler::xpathLiteral('a\'b"c');
+ * //prints concat('a', "'", 'b"c')
+ *
+ * @return string
+ */
+ public static function xpathLiteral(string $s)
+ {
+ if (!str_contains($s, "'")) {
+ return sprintf("'%s'", $s);
+ }
+
+ if (!str_contains($s, '"')) {
+ return sprintf('"%s"', $s);
+ }
+
+ $string = $s;
+ $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));
+ }
+
+ /**
+ * Filters the list of nodes with an XPath expression.
+ *
+ * The XPath expression should already be processed to apply it in the context of each node.
+ *
+ * @return static
+ */
+ private function filterRelativeXPath(string $xpath): object
+ {
+ $crawler = $this->createSubCrawler(null);
+ if (null === $this->document) {
+ return $crawler;
+ }
+
+ $domxpath = $this->createDOMXPath($this->document, $this->findNamespacePrefixes($xpath));
+
+ foreach ($this->nodes as $node) {
+ $crawler->add($domxpath->query($xpath, $node));
+ }
+
+ return $crawler;
+ }
+
+ /**
+ * Make the XPath relative to the current context.
+ *
+ * The returned XPath will match elements matching the XPath inside the current crawler
+ * when running in the context of a node of the crawler.
+ */
+ private function relativize(string $xpath): string
+ {
+ $expressions = [];
+
+ // An expression which will never match to replace expressions which cannot match in the crawler
+ // We cannot drop
+ $nonMatchingExpression = 'a[name() = "b"]';
+
+ $xpathLen = \strlen($xpath);
+ $openedBrackets = 0;
+ $startPosition = strspn($xpath, " \t\n\r\0\x0B");
+
+ for ($i = $startPosition; $i <= $xpathLen; ++$i) {
+ $i += strcspn($xpath, '"\'[]|', $i);
+
+ if ($i < $xpathLen) {
+ switch ($xpath[$i]) {
+ case '"':
+ case "'":
+ if (false === $i = strpos($xpath, $xpath[$i], $i + 1)) {
+ return $xpath; // The XPath expression is invalid
+ }
+ continue 2;
+ case '[':
+ ++$openedBrackets;
+ continue 2;
+ case ']':
+ --$openedBrackets;
+ continue 2;
+ }
+ }
+ if ($openedBrackets) {
+ continue;
+ }
+
+ if ($startPosition < $xpathLen && '(' === $xpath[$startPosition]) {
+ // If the union is inside some braces, we need to preserve the opening braces and apply
+ // the change only inside it.
+ $j = 1 + strspn($xpath, "( \t\n\r\0\x0B", $startPosition + 1);
+ $parenthesis = substr($xpath, $startPosition, $j);
+ $startPosition += $j;
+ } else {
+ $parenthesis = '';
+ }
+ $expression = rtrim(substr($xpath, $startPosition, $i - $startPosition));
+
+ if (str_starts_with($expression, 'self::*/')) {
+ $expression = './'.substr($expression, 8);
+ }
+
+ // add prefix before absolute element selector
+ if ('' === $expression) {
+ $expression = $nonMatchingExpression;
+ } elseif (str_starts_with($expression, '//')) {
+ $expression = 'descendant-or-self::'.substr($expression, 2);
+ } elseif (str_starts_with($expression, './/')) {
+ $expression = 'descendant-or-self::'.substr($expression, 3);
+ } elseif (str_starts_with($expression, './')) {
+ $expression = 'self::'.substr($expression, 2);
+ } elseif (str_starts_with($expression, 'child::')) {
+ $expression = 'self::'.substr($expression, 7);
+ } elseif ('/' === $expression[0] || '.' === $expression[0] || str_starts_with($expression, 'self::')) {
+ $expression = $nonMatchingExpression;
+ } elseif (str_starts_with($expression, 'descendant::')) {
+ $expression = 'descendant-or-self::'.substr($expression, 12);
+ } elseif (preg_match('/^(ancestor|ancestor-or-self|attribute|following|following-sibling|namespace|parent|preceding|preceding-sibling)::/', $expression)) {
+ // the fake root has no parent, preceding or following nodes and also no attributes (even no namespace attributes)
+ $expression = $nonMatchingExpression;
+ } elseif (!str_starts_with($expression, 'descendant-or-self::')) {
+ $expression = 'self::'.$expression;
+ }
+ $expressions[] = $parenthesis.$expression;
+
+ if ($i === $xpathLen) {
+ return implode(' | ', $expressions);
+ }
+
+ $i += strspn($xpath, " \t\n\r\0\x0B", $i + 1);
+ $startPosition = $i + 1;
+ }
+
+ return $xpath; // The XPath expression is invalid
+ }
+
+ /**
+ * @return \DOMNode|null
+ */
+ public function getNode(int $position)
+ {
+ return $this->nodes[$position] ?? null;
+ }
+
+ /**
+ * @return int
+ */
+ #[\ReturnTypeWillChange]
+ public function count()
+ {
+ return \count($this->nodes);
+ }
+
+ /**
+ * @return \ArrayIterator<int, \DOMNode>
+ */
+ #[\ReturnTypeWillChange]
+ public function getIterator()
+ {
+ return new \ArrayIterator($this->nodes);
+ }
+
+ /**
+ * @return array
+ */
+ protected function sibling(\DOMNode $node, string $siblingDir = 'nextSibling')
+ {
+ $nodes = [];
+
+ $currentNode = $this->getNode(0);
+ do {
+ if ($node !== $currentNode && \XML_ELEMENT_NODE === $node->nodeType) {
+ $nodes[] = $node;
+ }
+ } while ($node = $node->$siblingDir);
+
+ return $nodes;
+ }
+
+ private function parseHtml5(string $htmlContent, string $charset = 'UTF-8'): \DOMDocument
+ {
+ return $this->html5Parser->parse($this->convertToHtmlEntities($htmlContent, $charset));
+ }
+
+ private function parseXhtml(string $htmlContent, string $charset = 'UTF-8'): \DOMDocument
+ {
+ $htmlContent = $this->convertToHtmlEntities($htmlContent, $charset);
+
+ $internalErrors = libxml_use_internal_errors(true);
+ if (\LIBXML_VERSION < 20900) {
+ $disableEntities = libxml_disable_entity_loader(true);
+ }
+
+ $dom = new \DOMDocument('1.0', $charset);
+ $dom->validateOnParse = true;
+
+ if ('' !== trim($htmlContent)) {
+ @$dom->loadHTML($htmlContent);
+ }
+
+ libxml_use_internal_errors($internalErrors);
+ if (\LIBXML_VERSION < 20900) {
+ libxml_disable_entity_loader($disableEntities);
+ }
+
+ return $dom;
+ }
+
+ /**
+ * Converts charset to HTML-entities to ensure valid parsing.
+ */
+ private function convertToHtmlEntities(string $htmlContent, string $charset = 'UTF-8'): string
+ {
+ set_error_handler(function () { throw new \Exception(); });
+
+ try {
+ return mb_encode_numericentity($htmlContent, [0x80, 0x10FFFF, 0, 0x1FFFFF], $charset);
+ } catch (\Exception|\ValueError $e) {
+ try {
+ $htmlContent = iconv($charset, 'UTF-8', $htmlContent);
+ $htmlContent = mb_encode_numericentity($htmlContent, [0x80, 0x10FFFF, 0, 0x1FFFFF], 'UTF-8');
+ } catch (\Exception|\ValueError $e) {
+ }
+
+ return $htmlContent;
+ } finally {
+ restore_error_handler();
+ }
+ }
+
+ /**
+ * @throws \InvalidArgumentException
+ */
+ private function createDOMXPath(\DOMDocument $document, array $prefixes = []): \DOMXPath
+ {
+ $domxpath = new \DOMXPath($document);
+
+ foreach ($prefixes as $prefix) {
+ $namespace = $this->discoverNamespace($domxpath, $prefix);
+ if (null !== $namespace) {
+ $domxpath->registerNamespace($prefix, $namespace);
+ }
+ }
+
+ return $domxpath;
+ }
+
+ /**
+ * @throws \InvalidArgumentException
+ */
+ private function discoverNamespace(\DOMXPath $domxpath, string $prefix): ?string
+ {
+ if (\array_key_exists($prefix, $this->namespaces)) {
+ return $this->namespaces[$prefix];
+ }
+
+ if ($this->cachedNamespaces->offsetExists($prefix)) {
+ return $this->cachedNamespaces[$prefix];
+ }
+
+ // ask for one namespace, otherwise we'd get a collection with an item for each node
+ $namespaces = $domxpath->query(sprintf('(//namespace::*[name()="%s"])[last()]', $this->defaultNamespacePrefix === $prefix ? '' : $prefix));
+
+ return $this->cachedNamespaces[$prefix] = ($node = $namespaces->item(0)) ? $node->nodeValue : null;
+ }
+
+ private function findNamespacePrefixes(string $xpath): array
+ {
+ if (preg_match_all('/(?P<prefix>[a-z_][a-z_0-9\-\.]*+):[^"\/:]/i', $xpath, $matches)) {
+ return array_unique($matches['prefix']);
+ }
+
+ return [];
+ }
+
+ /**
+ * Creates a crawler for some subnodes.
+ *
+ * @param \DOMNodeList|\DOMNode|\DOMNode[]|string|null $nodes
+ *
+ * @return static
+ */
+ private function createSubCrawler($nodes): object
+ {
+ $crawler = new static($nodes, $this->uri, $this->baseHref);
+ $crawler->isHtml = $this->isHtml;
+ $crawler->document = $this->document;
+ $crawler->namespaces = $this->namespaces;
+ $crawler->cachedNamespaces = $this->cachedNamespaces;
+ $crawler->html5Parser = $this->html5Parser;
+
+ return $crawler;
+ }
+
+ /**
+ * @throws \LogicException If the CssSelector Component is not available
+ */
+ private function createCssSelectorConverter(): CssSelectorConverter
+ {
+ if (!class_exists(CssSelectorConverter::class)) {
+ throw new \LogicException('To filter with a CSS selector, install the CssSelector component ("composer require symfony/css-selector"). Or use filterXpath instead.');
+ }
+
+ return new CssSelectorConverter($this->isHtml);
+ }
+
+ /**
+ * Parse string into DOMDocument object using HTML5 parser if the content is HTML5 and the library is available.
+ * Use libxml parser otherwise.
+ */
+ private function parseHtmlString(string $content, string $charset): \DOMDocument
+ {
+ if ($this->canParseHtml5String($content)) {
+ return $this->parseHtml5($content, $charset);
+ }
+
+ return $this->parseXhtml($content, $charset);
+ }
+
+ private function canParseHtml5String(string $content): bool
+ {
+ if (null === $this->html5Parser) {
+ return false;
+ }
+ if (false === ($pos = stripos($content, '<!doctype html>'))) {
+ return false;
+ }
+ $header = substr($content, 0, $pos);
+
+ return '' === $header || $this->isValidHtml5Heading($header);
+ }
+
+ private function isValidHtml5Heading(string $heading): bool
+ {
+ return 1 === preg_match('/^\x{FEFF}?\s*(<!--[^>]*?-->\s*)*$/u', $heading);
+ }
+}
diff --git a/symfony/dom-crawler/Field/ChoiceFormField.php b/symfony/dom-crawler/Field/ChoiceFormField.php
new file mode 100644
index 00000000..9eed27ba
--- /dev/null
+++ b/symfony/dom-crawler/Field/ChoiceFormField.php
@@ -0,0 +1,321 @@
+<?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\DomCrawler\Field;
+
+/**
+ * ChoiceFormField represents a choice form field.
+ *
+ * It is constructed from an HTML select tag, or an HTML checkbox, or radio inputs.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ */
+class ChoiceFormField extends FormField
+{
+ /**
+ * @var string
+ */
+ private $type;
+ /**
+ * @var bool
+ */
+ private $multiple;
+ /**
+ * @var array
+ */
+ private $options;
+ /**
+ * @var bool
+ */
+ private $validationDisabled = false;
+
+ /**
+ * Returns true if the field should be included in the submitted values.
+ *
+ * @return bool true if the field should be included in the submitted values, false otherwise
+ */
+ public function hasValue()
+ {
+ // don't send a value for unchecked checkboxes
+ if (\in_array($this->type, ['checkbox', 'radio']) && null === $this->value) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Check if the current selected option is disabled.
+ *
+ * @return bool
+ */
+ public function isDisabled()
+ {
+ if (parent::isDisabled() && 'select' === $this->type) {
+ return true;
+ }
+
+ foreach ($this->options as $option) {
+ if ($option['value'] == $this->value && $option['disabled']) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Sets the value of the field.
+ *
+ * @param string|array $value The value of the field
+ */
+ public function select($value)
+ {
+ $this->setValue($value);
+ }
+
+ /**
+ * Ticks a checkbox.
+ *
+ * @throws \LogicException When the type provided is not correct
+ */
+ public function tick()
+ {
+ if ('checkbox' !== $this->type) {
+ throw new \LogicException(sprintf('You cannot tick "%s" as it is not a checkbox (%s).', $this->name, $this->type));
+ }
+
+ $this->setValue(true);
+ }
+
+ /**
+ * Unticks a checkbox.
+ *
+ * @throws \LogicException When the type provided is not correct
+ */
+ public function untick()
+ {
+ if ('checkbox' !== $this->type) {
+ throw new \LogicException(sprintf('You cannot untick "%s" as it is not a checkbox (%s).', $this->name, $this->type));
+ }
+
+ $this->setValue(false);
+ }
+
+ /**
+ * Sets the value of the field.
+ *
+ * @param string|array|bool|null $value The value of the field
+ *
+ * @throws \InvalidArgumentException When value type provided is not correct
+ */
+ public function setValue($value)
+ {
+ if ('checkbox' === $this->type && false === $value) {
+ // uncheck
+ $this->value = null;
+ } elseif ('checkbox' === $this->type && true === $value) {
+ // check
+ $this->value = $this->options[0]['value'];
+ } else {
+ if (\is_array($value)) {
+ if (!$this->multiple) {
+ throw new \InvalidArgumentException(sprintf('The value for "%s" cannot be an array.', $this->name));
+ }
+
+ foreach ($value as $v) {
+ if (!$this->containsOption($v, $this->options)) {
+ throw new \InvalidArgumentException(sprintf('Input "%s" cannot take "%s" as a value (possible values: "%s").', $this->name, $v, implode('", "', $this->availableOptionValues())));
+ }
+ }
+ } elseif (!$this->containsOption($value, $this->options)) {
+ throw new \InvalidArgumentException(sprintf('Input "%s" cannot take "%s" as a value (possible values: "%s").', $this->name, $value, implode('", "', $this->availableOptionValues())));
+ }
+
+ if ($this->multiple) {
+ $value = (array) $value;
+ }
+
+ if (\is_array($value)) {
+ $this->value = $value;
+ } else {
+ parent::setValue($value);
+ }
+ }
+ }
+
+ /**
+ * Adds a choice to the current ones.
+ *
+ * @throws \LogicException When choice provided is not multiple nor radio
+ *
+ * @internal
+ */
+ public function addChoice(\DOMElement $node)
+ {
+ if (!$this->multiple && 'radio' !== $this->type) {
+ throw new \LogicException(sprintf('Unable to add a choice for "%s" as it is not multiple or is not a radio button.', $this->name));
+ }
+
+ $option = $this->buildOptionValue($node);
+ $this->options[] = $option;
+
+ if ($node->hasAttribute('checked')) {
+ $this->value = $option['value'];
+ }
+ }
+
+ /**
+ * Returns the type of the choice field (radio, select, or checkbox).
+ *
+ * @return string
+ */
+ public function getType()
+ {
+ return $this->type;
+ }
+
+ /**
+ * Returns true if the field accepts multiple values.
+ *
+ * @return bool
+ */
+ public function isMultiple()
+ {
+ return $this->multiple;
+ }
+
+ /**
+ * Initializes the form field.
+ *
+ * @throws \LogicException When node type is incorrect
+ */
+ protected function initialize()
+ {
+ if ('input' !== $this->node->nodeName && 'select' !== $this->node->nodeName) {
+ throw new \LogicException(sprintf('A ChoiceFormField can only be created from an input or select tag (%s given).', $this->node->nodeName));
+ }
+
+ if ('input' === $this->node->nodeName && 'checkbox' !== strtolower($this->node->getAttribute('type')) && 'radio' !== strtolower($this->node->getAttribute('type'))) {
+ throw new \LogicException(sprintf('A ChoiceFormField can only be created from an input tag with a type of checkbox or radio (given type is "%s").', $this->node->getAttribute('type')));
+ }
+
+ $this->value = null;
+ $this->options = [];
+ $this->multiple = false;
+
+ if ('input' == $this->node->nodeName) {
+ $this->type = strtolower($this->node->getAttribute('type'));
+ $optionValue = $this->buildOptionValue($this->node);
+ $this->options[] = $optionValue;
+
+ if ($this->node->hasAttribute('checked')) {
+ $this->value = $optionValue['value'];
+ }
+ } else {
+ $this->type = 'select';
+ if ($this->node->hasAttribute('multiple')) {
+ $this->multiple = true;
+ $this->value = [];
+ $this->name = str_replace('[]', '', $this->name);
+ }
+
+ $found = false;
+ foreach ($this->xpath->query('descendant::option', $this->node) as $option) {
+ $optionValue = $this->buildOptionValue($option);
+ $this->options[] = $optionValue;
+
+ if ($option->hasAttribute('selected')) {
+ $found = true;
+ if ($this->multiple) {
+ $this->value[] = $optionValue['value'];
+ } else {
+ $this->value = $optionValue['value'];
+ }
+ }
+ }
+
+ // if no option is selected and if it is a simple select box, take the first option as the value
+ if (!$found && !$this->multiple && !empty($this->options)) {
+ $this->value = $this->options[0]['value'];
+ }
+ }
+ }
+
+ /**
+ * Returns option value with associated disabled flag.
+ */
+ private function buildOptionValue(\DOMElement $node): array
+ {
+ $option = [];
+
+ $defaultDefaultValue = 'select' === $this->node->nodeName ? '' : 'on';
+ $defaultValue = (isset($node->nodeValue) && !empty($node->nodeValue)) ? $node->nodeValue : $defaultDefaultValue;
+ $option['value'] = $node->hasAttribute('value') ? $node->getAttribute('value') : $defaultValue;
+ $option['disabled'] = $node->hasAttribute('disabled');
+
+ return $option;
+ }
+
+ /**
+ * Checks whether given value is in the existing options.
+ *
+ * @internal since Symfony 5.3
+ *
+ * @return bool
+ */
+ public function containsOption(string $optionValue, array $options)
+ {
+ if ($this->validationDisabled) {
+ return true;
+ }
+
+ foreach ($options as $option) {
+ if ($option['value'] == $optionValue) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns list of available field options.
+ *
+ * @internal since Symfony 5.3
+ *
+ * @return array
+ */
+ public function availableOptionValues()
+ {
+ $values = [];
+
+ foreach ($this->options as $option) {
+ $values[] = $option['value'];
+ }
+
+ return $values;
+ }
+
+ /**
+ * Disables the internal validation of the field.
+ *
+ * @internal since Symfony 5.3
+ *
+ * @return $this
+ */
+ public function disableValidation()
+ {
+ $this->validationDisabled = true;
+
+ return $this;
+ }
+}
diff --git a/symfony/dom-crawler/Field/FileFormField.php b/symfony/dom-crawler/Field/FileFormField.php
new file mode 100644
index 00000000..bd97e768
--- /dev/null
+++ b/symfony/dom-crawler/Field/FileFormField.php
@@ -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\DomCrawler\Field;
+
+/**
+ * FileFormField represents a file form field (an HTML file input tag).
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ */
+class FileFormField extends FormField
+{
+ /**
+ * Sets the PHP error code associated with the field.
+ *
+ * @param int $error The error code (one of UPLOAD_ERR_INI_SIZE, UPLOAD_ERR_FORM_SIZE, UPLOAD_ERR_PARTIAL, UPLOAD_ERR_NO_FILE, UPLOAD_ERR_NO_TMP_DIR, UPLOAD_ERR_CANT_WRITE, or UPLOAD_ERR_EXTENSION)
+ *
+ * @throws \InvalidArgumentException When error code doesn't exist
+ */
+ public function setErrorCode(int $error)
+ {
+ $codes = [\UPLOAD_ERR_INI_SIZE, \UPLOAD_ERR_FORM_SIZE, \UPLOAD_ERR_PARTIAL, \UPLOAD_ERR_NO_FILE, \UPLOAD_ERR_NO_TMP_DIR, \UPLOAD_ERR_CANT_WRITE, \UPLOAD_ERR_EXTENSION];
+ if (!\in_array($error, $codes)) {
+ throw new \InvalidArgumentException(sprintf('The error code "%s" is not valid.', $error));
+ }
+
+ $this->value = ['name' => '', 'type' => '', 'tmp_name' => '', 'error' => $error, 'size' => 0];
+ }
+
+ /**
+ * Sets the value of the field.
+ */
+ public function upload(?string $value)
+ {
+ $this->setValue($value);
+ }
+
+ /**
+ * Sets the value of the field.
+ */
+ public function setValue(?string $value)
+ {
+ if (null !== $value && is_readable($value)) {
+ $error = \UPLOAD_ERR_OK;
+ $size = filesize($value);
+ $info = pathinfo($value);
+ $name = $info['basename'];
+
+ // copy to a tmp location
+ $tmp = sys_get_temp_dir().'/'.strtr(substr(base64_encode(hash('sha256', uniqid(mt_rand(), true), true)), 0, 7), '/', '_');
+ if (\array_key_exists('extension', $info)) {
+ $tmp .= '.'.$info['extension'];
+ }
+ if (is_file($tmp)) {
+ unlink($tmp);
+ }
+ copy($value, $tmp);
+ $value = $tmp;
+ } else {
+ $error = \UPLOAD_ERR_NO_FILE;
+ $size = 0;
+ $name = '';
+ $value = '';
+ }
+
+ $this->value = ['name' => $name, 'type' => '', 'tmp_name' => $value, 'error' => $error, 'size' => $size];
+ }
+
+ /**
+ * Sets path to the file as string for simulating HTTP request.
+ */
+ public function setFilePath(string $path)
+ {
+ parent::setValue($path);
+ }
+
+ /**
+ * Initializes the form field.
+ *
+ * @throws \LogicException When node type is incorrect
+ */
+ protected function initialize()
+ {
+ if ('input' !== $this->node->nodeName) {
+ throw new \LogicException(sprintf('A FileFormField can only be created from an input tag (%s given).', $this->node->nodeName));
+ }
+
+ if ('file' !== strtolower($this->node->getAttribute('type'))) {
+ throw new \LogicException(sprintf('A FileFormField can only be created from an input tag with a type of file (given type is "%s").', $this->node->getAttribute('type')));
+ }
+
+ $this->setValue(null);
+ }
+}
diff --git a/symfony/dom-crawler/Field/FormField.php b/symfony/dom-crawler/Field/FormField.php
new file mode 100644
index 00000000..066af4a1
--- /dev/null
+++ b/symfony/dom-crawler/Field/FormField.php
@@ -0,0 +1,131 @@
+<?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\DomCrawler\Field;
+
+/**
+ * FormField is the abstract class for all form fields.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ */
+abstract class FormField
+{
+ /**
+ * @var \DOMElement
+ */
+ protected $node;
+ /**
+ * @var string
+ */
+ protected $name;
+ /**
+ * @var string
+ */
+ protected $value;
+ /**
+ * @var \DOMDocument
+ */
+ protected $document;
+ /**
+ * @var \DOMXPath
+ */
+ protected $xpath;
+ /**
+ * @var bool
+ */
+ protected $disabled;
+
+ /**
+ * @param \DOMElement $node The node associated with this field
+ */
+ public function __construct(\DOMElement $node)
+ {
+ $this->node = $node;
+ $this->name = $node->getAttribute('name');
+ $this->xpath = new \DOMXPath($node->ownerDocument);
+
+ $this->initialize();
+ }
+
+ /**
+ * Returns the label tag associated to the field or null if none.
+ *
+ * @return \DOMElement|null
+ */
+ public function getLabel()
+ {
+ $xpath = new \DOMXPath($this->node->ownerDocument);
+
+ if ($this->node->hasAttribute('id')) {
+ $labels = $xpath->query(sprintf('descendant::label[@for="%s"]', $this->node->getAttribute('id')));
+ if ($labels->length > 0) {
+ return $labels->item(0);
+ }
+ }
+
+ $labels = $xpath->query('ancestor::label[1]', $this->node);
+
+ return $labels->length > 0 ? $labels->item(0) : null;
+ }
+
+ /**
+ * Returns the name of the field.
+ *
+ * @return string
+ */
+ public function getName()
+ {
+ return $this->name;
+ }
+
+ /**
+ * Gets the value of the field.
+ *
+ * @return string|array|null
+ */
+ public function getValue()
+ {
+ return $this->value;
+ }
+
+ /**
+ * Sets the value of the field.
+ */
+ public function setValue(?string $value)
+ {
+ $this->value = $value ?? '';
+ }
+
+ /**
+ * Returns true if the field should be included in the submitted values.
+ *
+ * @return bool
+ */
+ public function hasValue()
+ {
+ return true;
+ }
+
+ /**
+ * Check if the current field is disabled.
+ *
+ * @return bool
+ */
+ public function isDisabled()
+ {
+ return $this->node->hasAttribute('disabled');
+ }
+
+ /**
+ * Initializes the form field.
+ */
+ abstract protected function initialize();
+}
diff --git a/symfony/dom-crawler/Field/InputFormField.php b/symfony/dom-crawler/Field/InputFormField.php
new file mode 100644
index 00000000..1c3c84d7
--- /dev/null
+++ b/symfony/dom-crawler/Field/InputFormField.php
@@ -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\DomCrawler\Field;
+
+/**
+ * InputFormField represents an input form field (an HTML input tag).
+ *
+ * For inputs with type of file, checkbox, or radio, there are other more
+ * specialized classes (cf. FileFormField and ChoiceFormField).
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ */
+class InputFormField extends FormField
+{
+ /**
+ * Initializes the form field.
+ *
+ * @throws \LogicException When node type is incorrect
+ */
+ protected function initialize()
+ {
+ if ('input' !== $this->node->nodeName && 'button' !== $this->node->nodeName) {
+ throw new \LogicException(sprintf('An InputFormField can only be created from an input or button tag (%s given).', $this->node->nodeName));
+ }
+
+ $type = strtolower($this->node->getAttribute('type'));
+ if ('checkbox' === $type) {
+ throw new \LogicException('Checkboxes should be instances of ChoiceFormField.');
+ }
+
+ if ('file' === $type) {
+ throw new \LogicException('File inputs should be instances of FileFormField.');
+ }
+
+ $this->value = $this->node->getAttribute('value');
+ }
+}
diff --git a/symfony/dom-crawler/Field/TextareaFormField.php b/symfony/dom-crawler/Field/TextareaFormField.php
new file mode 100644
index 00000000..15526e1c
--- /dev/null
+++ b/symfony/dom-crawler/Field/TextareaFormField.php
@@ -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\DomCrawler\Field;
+
+/**
+ * TextareaFormField represents a textarea form field (an HTML textarea tag).
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ */
+class TextareaFormField extends FormField
+{
+ /**
+ * Initializes the form field.
+ *
+ * @throws \LogicException When node type is incorrect
+ */
+ protected function initialize()
+ {
+ if ('textarea' !== $this->node->nodeName) {
+ throw new \LogicException(sprintf('A TextareaFormField can only be created from a textarea tag (%s given).', $this->node->nodeName));
+ }
+
+ $this->value = '';
+ foreach ($this->node->childNodes as $node) {
+ $this->value .= $node->wholeText;
+ }
+ }
+}
diff --git a/symfony/dom-crawler/Form.php b/symfony/dom-crawler/Form.php
new file mode 100644
index 00000000..ebad35b3
--- /dev/null
+++ b/symfony/dom-crawler/Form.php
@@ -0,0 +1,501 @@
+<?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\DomCrawler;
+
+use Symfony\Component\DomCrawler\Field\ChoiceFormField;
+use Symfony\Component\DomCrawler\Field\FormField;
+
+/**
+ * Form represents an HTML form.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ */
+class Form extends Link implements \ArrayAccess
+{
+ /**
+ * @var \DOMElement
+ */
+ private $button;
+
+ /**
+ * @var FormFieldRegistry
+ */
+ private $fields;
+
+ /**
+ * @var string
+ */
+ private $baseHref;
+
+ /**
+ * @param \DOMElement $node A \DOMElement instance
+ * @param string|null $currentUri The URI of the page where the form is embedded
+ * @param string|null $method The method to use for the link (if null, it defaults to the method defined by the form)
+ * @param string|null $baseHref The URI of the <base> used for relative links, but not for empty action
+ *
+ * @throws \LogicException if the node is not a button inside a form tag
+ */
+ public function __construct(\DOMElement $node, string $currentUri = null, string $method = null, string $baseHref = null)
+ {
+ parent::__construct($node, $currentUri, $method);
+ $this->baseHref = $baseHref;
+
+ $this->initialize();
+ }
+
+ /**
+ * Gets the form node associated with this form.
+ *
+ * @return \DOMElement
+ */
+ public function getFormNode()
+ {
+ return $this->node;
+ }
+
+ /**
+ * Sets the value of the fields.
+ *
+ * @param array $values An array of field values
+ *
+ * @return $this
+ */
+ public function setValues(array $values)
+ {
+ foreach ($values as $name => $value) {
+ $this->fields->set($name, $value);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Gets the field values.
+ *
+ * The returned array does not include file fields (@see getFiles).
+ *
+ * @return array
+ */
+ public function getValues()
+ {
+ $values = [];
+ foreach ($this->fields->all() as $name => $field) {
+ if ($field->isDisabled()) {
+ continue;
+ }
+
+ if (!$field instanceof Field\FileFormField && $field->hasValue()) {
+ $values[$name] = $field->getValue();
+ }
+ }
+
+ return $values;
+ }
+
+ /**
+ * Gets the file field values.
+ *
+ * @return array
+ */
+ public function getFiles()
+ {
+ if (!\in_array($this->getMethod(), ['POST', 'PUT', 'DELETE', 'PATCH'])) {
+ return [];
+ }
+
+ $files = [];
+
+ foreach ($this->fields->all() as $name => $field) {
+ if ($field->isDisabled()) {
+ continue;
+ }
+
+ if ($field instanceof Field\FileFormField) {
+ $files[$name] = $field->getValue();
+ }
+ }
+
+ return $files;
+ }
+
+ /**
+ * Gets the field values as PHP.
+ *
+ * This method converts fields with the array notation
+ * (like foo[bar] to arrays) like PHP does.
+ *
+ * @return array
+ */
+ public function getPhpValues()
+ {
+ $values = [];
+ foreach ($this->getValues() as $name => $value) {
+ $qs = http_build_query([$name => $value], '', '&');
+ if (!empty($qs)) {
+ parse_str($qs, $expandedValue);
+ $varName = substr($name, 0, \strlen(key($expandedValue)));
+ $values[] = [$varName => current($expandedValue)];
+ }
+ }
+
+ return array_replace_recursive([], ...$values);
+ }
+
+ /**
+ * Gets the file field values as PHP.
+ *
+ * This method converts fields with the array notation
+ * (like foo[bar] to arrays) like PHP does.
+ * The returned array is consistent with the array for field values
+ * (@see getPhpValues), rather than uploaded files found in $_FILES.
+ * For a compound file field foo[bar] it will create foo[bar][name],
+ * instead of foo[name][bar] which would be found in $_FILES.
+ *
+ * @return array
+ */
+ public function getPhpFiles()
+ {
+ $values = [];
+ foreach ($this->getFiles() as $name => $value) {
+ $qs = http_build_query([$name => $value], '', '&');
+ if (!empty($qs)) {
+ parse_str($qs, $expandedValue);
+ $varName = substr($name, 0, \strlen(key($expandedValue)));
+
+ array_walk_recursive(
+ $expandedValue,
+ function (&$value, $key) {
+ if (ctype_digit($value) && ('size' === $key || 'error' === $key)) {
+ $value = (int) $value;
+ }
+ }
+ );
+
+ reset($expandedValue);
+
+ $values[] = [$varName => current($expandedValue)];
+ }
+ }
+
+ return array_replace_recursive([], ...$values);
+ }
+
+ /**
+ * Gets the URI of the form.
+ *
+ * The returned URI is not the same as the form "action" attribute.
+ * This method merges the value if the method is GET to mimics
+ * browser behavior.
+ *
+ * @return string
+ */
+ public function getUri()
+ {
+ $uri = parent::getUri();
+
+ if (!\in_array($this->getMethod(), ['POST', 'PUT', 'DELETE', 'PATCH'])) {
+ $query = parse_url($uri, \PHP_URL_QUERY);
+ $currentParameters = [];
+ if ($query) {
+ parse_str($query, $currentParameters);
+ }
+
+ $queryString = http_build_query(array_merge($currentParameters, $this->getValues()), '', '&');
+
+ $pos = strpos($uri, '?');
+ $base = false === $pos ? $uri : substr($uri, 0, $pos);
+ $uri = rtrim($base.'?'.$queryString, '?');
+ }
+
+ return $uri;
+ }
+
+ protected function getRawUri()
+ {
+ // If the form was created from a button rather than the form node, check for HTML5 action overrides
+ if ($this->button !== $this->node && $this->button->getAttribute('formaction')) {
+ return $this->button->getAttribute('formaction');
+ }
+
+ return $this->node->getAttribute('action');
+ }
+
+ /**
+ * Gets the form method.
+ *
+ * If no method is defined in the form, GET is returned.
+ *
+ * @return string
+ */
+ public function getMethod()
+ {
+ if (null !== $this->method) {
+ return $this->method;
+ }
+
+ // If the form was created from a button rather than the form node, check for HTML5 method override
+ if ($this->button !== $this->node && $this->button->getAttribute('formmethod')) {
+ return strtoupper($this->button->getAttribute('formmethod'));
+ }
+
+ return $this->node->getAttribute('method') ? strtoupper($this->node->getAttribute('method')) : 'GET';
+ }
+
+ /**
+ * Gets the form name.
+ *
+ * If no name is defined on the form, an empty string is returned.
+ */
+ public function getName(): string
+ {
+ return $this->node->getAttribute('name');
+ }
+
+ /**
+ * Returns true if the named field exists.
+ *
+ * @return bool
+ */
+ public function has(string $name)
+ {
+ return $this->fields->has($name);
+ }
+
+ /**
+ * Removes a field from the form.
+ */
+ public function remove(string $name)
+ {
+ $this->fields->remove($name);
+ }
+
+ /**
+ * Gets a named field.
+ *
+ * @return FormField|FormField[]|FormField[][]
+ *
+ * @throws \InvalidArgumentException When field is not present in this form
+ */
+ public function get(string $name)
+ {
+ return $this->fields->get($name);
+ }
+
+ /**
+ * Sets a named field.
+ */
+ public function set(FormField $field)
+ {
+ $this->fields->add($field);
+ }
+
+ /**
+ * Gets all fields.
+ *
+ * @return FormField[]
+ */
+ public function all()
+ {
+ return $this->fields->all();
+ }
+
+ /**
+ * Returns true if the named field exists.
+ *
+ * @param string $name The field name
+ *
+ * @return bool
+ */
+ #[\ReturnTypeWillChange]
+ public function offsetExists($name)
+ {
+ return $this->has($name);
+ }
+
+ /**
+ * Gets the value of a field.
+ *
+ * @param string $name The field name
+ *
+ * @return FormField|FormField[]|FormField[][]
+ *
+ * @throws \InvalidArgumentException if the field does not exist
+ */
+ #[\ReturnTypeWillChange]
+ public function offsetGet($name)
+ {
+ return $this->fields->get($name);
+ }
+
+ /**
+ * Sets the value of a field.
+ *
+ * @param string $name The field name
+ * @param string|array $value The value of the field
+ *
+ * @return void
+ *
+ * @throws \InvalidArgumentException if the field does not exist
+ */
+ #[\ReturnTypeWillChange]
+ public function offsetSet($name, $value)
+ {
+ $this->fields->set($name, $value);
+ }
+
+ /**
+ * Removes a field from the form.
+ *
+ * @param string $name The field name
+ *
+ * @return void
+ */
+ #[\ReturnTypeWillChange]
+ public function offsetUnset($name)
+ {
+ $this->fields->remove($name);
+ }
+
+ /**
+ * Disables validation.
+ *
+ * @return $this
+ */
+ public function disableValidation()
+ {
+ foreach ($this->fields->all() as $field) {
+ if ($field instanceof Field\ChoiceFormField) {
+ $field->disableValidation();
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * Sets the node for the form.
+ *
+ * Expects a 'submit' button \DOMElement and finds the corresponding form element, or the form element itself.
+ *
+ * @throws \LogicException If given node is not a button or input or does not have a form ancestor
+ */
+ protected function setNode(\DOMElement $node)
+ {
+ $this->button = $node;
+ if ('button' === $node->nodeName || ('input' === $node->nodeName && \in_array(strtolower($node->getAttribute('type')), ['submit', 'button', 'image']))) {
+ if ($node->hasAttribute('form')) {
+ // if the node has the HTML5-compliant 'form' attribute, use it
+ $formId = $node->getAttribute('form');
+ $form = $node->ownerDocument->getElementById($formId);
+ if (null === $form) {
+ throw new \LogicException(sprintf('The selected node has an invalid form attribute (%s).', $formId));
+ }
+ $this->node = $form;
+
+ return;
+ }
+ // we loop until we find a form ancestor
+ do {
+ if (null === $node = $node->parentNode) {
+ throw new \LogicException('The selected node does not have a form ancestor.');
+ }
+ } while ('form' !== $node->nodeName);
+ } elseif ('form' !== $node->nodeName) {
+ throw new \LogicException(sprintf('Unable to submit on a "%s" tag.', $node->nodeName));
+ }
+
+ $this->node = $node;
+ }
+
+ /**
+ * Adds form elements related to this form.
+ *
+ * Creates an internal copy of the submitted 'button' element and
+ * the form node or the entire document depending on whether we need
+ * to find non-descendant elements through HTML5 'form' attribute.
+ */
+ private function initialize()
+ {
+ $this->fields = new FormFieldRegistry();
+
+ $xpath = new \DOMXPath($this->node->ownerDocument);
+
+ // add submitted button if it has a valid name
+ if ('form' !== $this->button->nodeName && $this->button->hasAttribute('name') && $this->button->getAttribute('name')) {
+ if ('input' == $this->button->nodeName && 'image' == strtolower($this->button->getAttribute('type'))) {
+ $name = $this->button->getAttribute('name');
+ $this->button->setAttribute('value', '0');
+
+ // temporarily change the name of the input node for the x coordinate
+ $this->button->setAttribute('name', $name.'.x');
+ $this->set(new Field\InputFormField($this->button));
+
+ // temporarily change the name of the input node for the y coordinate
+ $this->button->setAttribute('name', $name.'.y');
+ $this->set(new Field\InputFormField($this->button));
+
+ // restore the original name of the input node
+ $this->button->setAttribute('name', $name);
+ } else {
+ $this->set(new Field\InputFormField($this->button));
+ }
+ }
+
+ // find form elements corresponding to the current form
+ if ($this->node->hasAttribute('id')) {
+ // corresponding elements are either descendants or have a matching HTML5 form attribute
+ $formId = Crawler::xpathLiteral($this->node->getAttribute('id'));
+
+ $fieldNodes = $xpath->query(sprintf('( descendant::input[@form=%s] | descendant::button[@form=%1$s] | descendant::textarea[@form=%1$s] | descendant::select[@form=%1$s] | //form[@id=%1$s]//input[not(@form)] | //form[@id=%1$s]//button[not(@form)] | //form[@id=%1$s]//textarea[not(@form)] | //form[@id=%1$s]//select[not(@form)] )[not(ancestor::template)]', $formId));
+ foreach ($fieldNodes as $node) {
+ $this->addField($node);
+ }
+ } else {
+ // do the xpath query with $this->node as the context node, to only find descendant elements
+ // however, descendant elements with form attribute are not part of this form
+ $fieldNodes = $xpath->query('( descendant::input[not(@form)] | descendant::button[not(@form)] | descendant::textarea[not(@form)] | descendant::select[not(@form)] )[not(ancestor::template)]', $this->node);
+ foreach ($fieldNodes as $node) {
+ $this->addField($node);
+ }
+ }
+
+ if ($this->baseHref && '' !== $this->node->getAttribute('action')) {
+ $this->currentUri = $this->baseHref;
+ }
+ }
+
+ private function addField(\DOMElement $node)
+ {
+ if (!$node->hasAttribute('name') || !$node->getAttribute('name')) {
+ return;
+ }
+
+ $nodeName = $node->nodeName;
+ if ('select' == $nodeName || 'input' == $nodeName && 'checkbox' == strtolower($node->getAttribute('type'))) {
+ $this->set(new Field\ChoiceFormField($node));
+ } elseif ('input' == $nodeName && 'radio' == strtolower($node->getAttribute('type'))) {
+ // there may be other fields with the same name that are no choice
+ // fields already registered (see https://github.com/symfony/symfony/issues/11689)
+ if ($this->has($node->getAttribute('name')) && $this->get($node->getAttribute('name')) instanceof ChoiceFormField) {
+ $this->get($node->getAttribute('name'))->addChoice($node);
+ } else {
+ $this->set(new Field\ChoiceFormField($node));
+ }
+ } elseif ('input' == $nodeName && 'file' == strtolower($node->getAttribute('type'))) {
+ $this->set(new Field\FileFormField($node));
+ } elseif ('input' == $nodeName && !\in_array(strtolower($node->getAttribute('type')), ['submit', 'button', 'image'])) {
+ $this->set(new Field\InputFormField($node));
+ } elseif ('textarea' == $nodeName) {
+ $this->set(new Field\TextareaFormField($node));
+ }
+ }
+}
diff --git a/symfony/dom-crawler/FormFieldRegistry.php b/symfony/dom-crawler/FormFieldRegistry.php
new file mode 100644
index 00000000..93522adc
--- /dev/null
+++ b/symfony/dom-crawler/FormFieldRegistry.php
@@ -0,0 +1,178 @@
+<?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\DomCrawler;
+
+use Symfony\Component\DomCrawler\Field\FormField;
+
+/**
+ * This is an internal class that must not be used directly.
+ *
+ * @internal
+ */
+class FormFieldRegistry
+{
+ private $fields = [];
+
+ private $base = '';
+
+ /**
+ * Adds a field to the registry.
+ */
+ public function add(FormField $field)
+ {
+ $segments = $this->getSegments($field->getName());
+
+ $target = &$this->fields;
+ while ($segments) {
+ if (!\is_array($target)) {
+ $target = [];
+ }
+ $path = array_shift($segments);
+ if ('' === $path) {
+ $target = &$target[];
+ } else {
+ $target = &$target[$path];
+ }
+ }
+ $target = $field;
+ }
+
+ /**
+ * Removes a field based on the fully qualifed name and its children from the registry.
+ */
+ public function remove(string $name)
+ {
+ $segments = $this->getSegments($name);
+ $target = &$this->fields;
+ while (\count($segments) > 1) {
+ $path = array_shift($segments);
+ if (!\is_array($target) || !\array_key_exists($path, $target)) {
+ return;
+ }
+ $target = &$target[$path];
+ }
+ unset($target[array_shift($segments)]);
+ }
+
+ /**
+ * Returns the value of the field based on the fully qualifed name and its children.
+ *
+ * @return FormField|FormField[]|FormField[][]
+ *
+ * @throws \InvalidArgumentException if the field does not exist
+ */
+ public function &get(string $name)
+ {
+ $segments = $this->getSegments($name);
+ $target = &$this->fields;
+ while ($segments) {
+ $path = array_shift($segments);
+ if (!\is_array($target) || !\array_key_exists($path, $target)) {
+ throw new \InvalidArgumentException(sprintf('Unreachable field "%s".', $path));
+ }
+ $target = &$target[$path];
+ }
+
+ return $target;
+ }
+
+ /**
+ * Tests whether the form has the given field based on the fully qualified name.
+ */
+ public function has(string $name): bool
+ {
+ try {
+ $this->get($name);
+
+ return true;
+ } catch (\InvalidArgumentException $e) {
+ return false;
+ }
+ }
+
+ /**
+ * Set the value of a field based on the fully qualified name and its children.
+ *
+ * @param mixed $value The value
+ *
+ * @throws \InvalidArgumentException if the field does not exist
+ */
+ public function set(string $name, $value)
+ {
+ $target = &$this->get($name);
+ if ((!\is_array($value) && $target instanceof Field\FormField) || $target instanceof Field\ChoiceFormField) {
+ $target->setValue($value);
+ } elseif (\is_array($value)) {
+ $registry = new static();
+ $registry->base = $name;
+ $registry->fields = $value;
+ foreach ($registry->all() as $k => $v) {
+ $this->set($k, $v);
+ }
+ } else {
+ throw new \InvalidArgumentException(sprintf('Cannot set value on a compound field "%s".', $name));
+ }
+ }
+
+ /**
+ * Returns the list of field with their value.
+ *
+ * @return FormField[] The list of fields as [string] Fully qualified name => (mixed) value)
+ */
+ public function all(): array
+ {
+ return $this->walk($this->fields, $this->base);
+ }
+
+ /**
+ * Transforms a PHP array in a list of fully qualified name / value.
+ */
+ private function walk(array $array, ?string $base = '', array &$output = []): array
+ {
+ foreach ($array as $k => $v) {
+ $path = empty($base) ? $k : sprintf('%s[%s]', $base, $k);
+ if (\is_array($v)) {
+ $this->walk($v, $path, $output);
+ } else {
+ $output[$path] = $v;
+ }
+ }
+
+ return $output;
+ }
+
+ /**
+ * Splits a field name into segments as a web browser would do.
+ *
+ * getSegments('base[foo][3][]') = ['base', 'foo, '3', ''];
+ *
+ * @return string[]
+ */
+ private function getSegments(string $name): array
+ {
+ if (preg_match('/^(?P<base>[^[]+)(?P<extra>(\[.*)|$)/', $name, $m)) {
+ $segments = [$m['base']];
+ while (!empty($m['extra'])) {
+ $extra = $m['extra'];
+ if (preg_match('/^\[(?P<segment>.*?)\](?P<extra>.*)$/', $extra, $m)) {
+ $segments[] = $m['segment'];
+ } else {
+ $segments[] = $extra;
+ }
+ }
+
+ return $segments;
+ }
+
+ return [$name];
+ }
+}
diff --git a/symfony/dom-crawler/Image.php b/symfony/dom-crawler/Image.php
new file mode 100644
index 00000000..b1ac5ca2
--- /dev/null
+++ b/symfony/dom-crawler/Image.php
@@ -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\DomCrawler;
+
+/**
+ * Image represents an HTML image (an HTML img tag).
+ */
+class Image extends AbstractUriElement
+{
+ public function __construct(\DOMElement $node, string $currentUri = null)
+ {
+ parent::__construct($node, $currentUri, 'GET');
+ }
+
+ protected function getRawUri()
+ {
+ return $this->node->getAttribute('src');
+ }
+
+ protected function setNode(\DOMElement $node)
+ {
+ if ('img' !== $node->nodeName) {
+ throw new \LogicException(sprintf('Unable to visualize a "%s" tag.', $node->nodeName));
+ }
+
+ $this->node = $node;
+ }
+}
diff --git a/symfony/dom-crawler/LICENSE b/symfony/dom-crawler/LICENSE
new file mode 100644
index 00000000..88bf75bb
--- /dev/null
+++ b/symfony/dom-crawler/LICENSE
@@ -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.
diff --git a/symfony/dom-crawler/Link.php b/symfony/dom-crawler/Link.php
new file mode 100644
index 00000000..80a356e4
--- /dev/null
+++ b/symfony/dom-crawler/Link.php
@@ -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\DomCrawler;
+
+/**
+ * Link represents an HTML link (an HTML a, area or link tag).
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ */
+class Link extends AbstractUriElement
+{
+ protected function getRawUri()
+ {
+ return $this->node->getAttribute('href');
+ }
+
+ protected function setNode(\DOMElement $node)
+ {
+ if ('a' !== $node->nodeName && 'area' !== $node->nodeName && 'link' !== $node->nodeName) {
+ throw new \LogicException(sprintf('Unable to navigate from a "%s" tag.', $node->nodeName));
+ }
+
+ $this->node = $node;
+ }
+}
diff --git a/symfony/dom-crawler/README.md b/symfony/dom-crawler/README.md
new file mode 100644
index 00000000..c77a5e39
--- /dev/null
+++ b/symfony/dom-crawler/README.md
@@ -0,0 +1,13 @@
+DomCrawler Component
+====================
+
+The DomCrawler component eases DOM navigation for HTML and XML documents.
+
+Resources
+---------
+
+ * [Documentation](https://symfony.com/doc/current/components/dom_crawler.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)
diff --git a/symfony/dom-crawler/UriResolver.php b/symfony/dom-crawler/UriResolver.php
new file mode 100644
index 00000000..be64f525
--- /dev/null
+++ b/symfony/dom-crawler/UriResolver.php
@@ -0,0 +1,136 @@
+<?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\DomCrawler;
+
+/**
+ * The UriResolver class takes an URI (relative, absolute, fragment, etc.)
+ * and turns it into an absolute URI against another given base URI.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ * @author Grégoire Pineau <lyrixx@lyrixx.info>
+ */
+class UriResolver
+{
+ /**
+ * Resolves a URI according to a base URI.
+ *
+ * For example if $uri=/foo/bar and $baseUri=https://symfony.com it will
+ * return https://symfony.com/foo/bar
+ *
+ * If the $uri is not absolute you must pass an absolute $baseUri
+ */
+ public static function resolve(string $uri, ?string $baseUri): string
+ {
+ $uri = trim($uri);
+
+ // absolute URL?
+ if (null !== parse_url($uri, \PHP_URL_SCHEME)) {
+ return $uri;
+ }
+
+ if (null === $baseUri) {
+ throw new \InvalidArgumentException('The URI is relative, so you must define its base URI passing an absolute URL.');
+ }
+
+ // empty URI
+ if (!$uri) {
+ return $baseUri;
+ }
+
+ // an anchor
+ if ('#' === $uri[0]) {
+ return self::cleanupAnchor($baseUri).$uri;
+ }
+
+ $baseUriCleaned = self::cleanupUri($baseUri);
+
+ if ('?' === $uri[0]) {
+ return $baseUriCleaned.$uri;
+ }
+
+ // absolute URL with relative schema
+ if (0 === strpos($uri, '//')) {
+ return preg_replace('#^([^/]*)//.*$#', '$1', $baseUriCleaned).$uri;
+ }
+
+ $baseUriCleaned = preg_replace('#^(.*?//[^/]*)(?:\/.*)?$#', '$1', $baseUriCleaned);
+
+ // absolute path
+ if ('/' === $uri[0]) {
+ return $baseUriCleaned.$uri;
+ }
+
+ // relative path
+ $path = parse_url(substr($baseUri, \strlen($baseUriCleaned)), \PHP_URL_PATH);
+ $path = self::canonicalizePath(substr($path, 0, strrpos($path, '/')).'/'.$uri);
+
+ return $baseUriCleaned.('' === $path || '/' !== $path[0] ? '/' : '').$path;
+ }
+
+ /**
+ * Returns the canonicalized URI path (see RFC 3986, section 5.2.4).
+ */
+ private static function canonicalizePath(string $path): string
+ {
+ if ('' === $path || '/' === $path) {
+ return $path;
+ }
+
+ if ('.' === substr($path, -1)) {
+ $path .= '/';
+ }
+
+ $output = [];
+
+ foreach (explode('/', $path) as $segment) {
+ if ('..' === $segment) {
+ array_pop($output);
+ } elseif ('.' !== $segment) {
+ $output[] = $segment;
+ }
+ }
+
+ return implode('/', $output);
+ }
+
+ /**
+ * Removes the query string and the anchor from the given uri.
+ */
+ private static function cleanupUri(string $uri): string
+ {
+ return self::cleanupQuery(self::cleanupAnchor($uri));
+ }
+
+ /**
+ * Removes the query string from the uri.
+ */
+ private static function cleanupQuery(string $uri): string
+ {
+ if (false !== $pos = strpos($uri, '?')) {
+ return substr($uri, 0, $pos);
+ }
+
+ return $uri;
+ }
+
+ /**
+ * Removes the anchor from the uri.
+ */
+ private static function cleanupAnchor(string $uri): string
+ {
+ if (false !== $pos = strpos($uri, '#')) {
+ return substr($uri, 0, $pos);
+ }
+
+ return $uri;
+ }
+}
diff --git a/symfony/dom-crawler/composer.json b/symfony/dom-crawler/composer.json
new file mode 100644
index 00000000..f89432da
--- /dev/null
+++ b/symfony/dom-crawler/composer.json
@@ -0,0 +1,42 @@
+{
+ "name": "symfony/dom-crawler",
+ "type": "library",
+ "description": "Eases DOM navigation for HTML and XML documents",
+ "keywords": [],
+ "homepage": "https://symfony.com",
+ "license": "MIT",
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "require": {
+ "php": ">=7.2.5",
+ "symfony/deprecation-contracts": "^2.1|^3",
+ "symfony/polyfill-ctype": "~1.8",
+ "symfony/polyfill-mbstring": "~1.0",
+ "symfony/polyfill-php80": "^1.16"
+ },
+ "require-dev": {
+ "symfony/css-selector": "^4.4|^5.0|^6.0",
+ "masterminds/html5": "^2.6"
+ },
+ "conflict": {
+ "masterminds/html5": "<2.6"
+ },
+ "suggest": {
+ "symfony/css-selector": ""
+ },
+ "autoload": {
+ "psr-4": { "Symfony\\Component\\DomCrawler\\": "" },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "minimum-stability": "dev"
+}