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-10 11:36:43 +0300
committerJulius Härtl <jus@bitgrid.net>2022-08-30 10:07:42 +0300
commit20f0c1b22572509a140845d8df25a96d2adb1a4d (patch)
tree460f87323db88a5c6f34b5e7e7ac33d529d39dd4
parent020d0d3892bd3b7296db8ed21448c834d33d5723 (diff)
Add fusonic/opengraph
Signed-off-by: Julius Härtl <jus@bitgrid.net>
-rw-r--r--.gitignore6
-rw-r--r--composer.json3
-rw-r--r--composer.lock249
-rw-r--r--composer/autoload_classmap.php85
-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.php115
-rw-r--r--composer/installed.json257
-rw-r--r--composer/installed.php40
-rw-r--r--composer/platform_check.php4
-rw-r--r--fusonic/linq/.gitignore4
-rw-r--r--fusonic/linq/LICENSE20
-rw-r--r--fusonic/linq/README.md207
-rw-r--r--fusonic/linq/composer.json22
-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/.gitattributes1
-rw-r--r--fusonic/opengraph/.gitignore3
-rw-r--r--fusonic/opengraph/.scrutinizer.yml17
-rw-r--r--fusonic/opengraph/LICENSE20
-rw-r--r--fusonic/opengraph/README.md142
-rw-r--r--fusonic/opengraph/composer.json42
-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
108 files changed, 10597 insertions, 15 deletions
diff --git a/.gitignore b/.gitignore
index 42e772fa..8287730c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -58,6 +58,12 @@ doctrine/inflector/tests
doctrine/lexer/composer.json
doctrine/lexer/LICENSE
+fusonic/linq/examples/
+fusonic/linq/tests/
+
+fusonic/opengraph/examples/
+fusonic/opengraph/phpunit.xml
+
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..24a3b069 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,70 @@ 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\\Test\\Constraint\\CrawlerSelectorAttributeValueSame' => $vendorDir . '/symfony/dom-crawler/Test/Constraint/CrawlerSelectorAttributeValueSame.php',
+ 'Symfony\\Component\\DomCrawler\\Test\\Constraint\\CrawlerSelectorExists' => $vendorDir . '/symfony/dom-crawler/Test/Constraint/CrawlerSelectorExists.php',
+ 'Symfony\\Component\\DomCrawler\\Test\\Constraint\\CrawlerSelectorTextContains' => $vendorDir . '/symfony/dom-crawler/Test/Constraint/CrawlerSelectorTextContains.php',
+ 'Symfony\\Component\\DomCrawler\\Test\\Constraint\\CrawlerSelectorTextSame' => $vendorDir . '/symfony/dom-crawler/Test/Constraint/CrawlerSelectorTextSame.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..bfe195c8 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,70 @@ 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\\Test\\Constraint\\CrawlerSelectorAttributeValueSame' => __DIR__ . '/..' . '/symfony/dom-crawler/Test/Constraint/CrawlerSelectorAttributeValueSame.php',
+ 'Symfony\\Component\\DomCrawler\\Test\\Constraint\\CrawlerSelectorExists' => __DIR__ . '/..' . '/symfony/dom-crawler/Test/Constraint/CrawlerSelectorExists.php',
+ 'Symfony\\Component\\DomCrawler\\Test\\Constraint\\CrawlerSelectorTextContains' => __DIR__ . '/..' . '/symfony/dom-crawler/Test/Constraint/CrawlerSelectorTextContains.php',
+ 'Symfony\\Component\\DomCrawler\\Test\\Constraint\\CrawlerSelectorTextSame' => __DIR__ . '/..' . '/symfony/dom-crawler/Test/Constraint/CrawlerSelectorTextSame.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/.gitignore b/fusonic/linq/.gitignore
new file mode 100644
index 00000000..9b846756
--- /dev/null
+++ b/fusonic/linq/.gitignore
@@ -0,0 +1,4 @@
+.idea/
+vendor/
+composer.lock
+composer.phar
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/README.md b/fusonic/linq/README.md
new file mode 100644
index 00000000..7fe2201c
--- /dev/null
+++ b/fusonic/linq/README.md
@@ -0,0 +1,207 @@
+# fusonic/linq
+
+[![Build Status](https://travis-ci.org/fusonic/linq.png)](https://travis-ci.org/fusonic/linq)
+[![Total Downloads](https://poser.pugx.org/fusonic/linq/downloads.png)](https://packagist.org/packages/fusonic/linq)
+
+fusonic/linq is a lightweight PHP library inspired by the LINQ 2 Objects extension methods in .NET.
+
+For a full introduction read my blog-post: http://www.fusonic.net/en/blog/2013/08/14/fusonic-linq-write-less-do-more/
+
+LINQ queries offer three main advantages over traditional foreach loops:
+
+* They are more concise and readable, especially when filtering multiple conditions.
+
+* They provide powerful filtering, ordering, and grouping capabilities with a minimum of application code.
+
+* In general, the more complex the operation you want to perform on the data, the more benefit you will realize by using LINQ instead of traditional iteration techniques.
+
+## Requirements
+
+fusonic/linq is supported on PHP 5.3 and up.
+
+
+## Installation & Usage
+
+The most flexible installation method is using Composer: Simply create a composer.json file in the root of your project:
+``` json
+{
+ "require": {
+ "fusonic/linq": "@dev"
+ }
+}
+```
+
+Install composer and run install command:
+``` bash
+curl -s http://getcomposer.org/installer | php
+php composer.phar install
+```
+
+Once installed, include vendor/autoload.php in your script to autoload fusonic/linq.
+
+``` php
+require 'vendor/autoload.php';
+use Fusonic\Linq\Linq;
+
+Linq::from(array())->count();
+```
+
+## Examples
+
+### Calculate the average file size of files in a directory:
+``` php
+$source = glob("files/*");
+Linq::from($source)
+ ->select(function($i) { return filesize($i); })
+ ->average();
+```
+
+### Find all files bigger than 1024 bytes and return the fileinfo object:
+``` php
+$source = glob("files/*");
+Linq::from($source)
+ ->where(function($i) { return filesize($i) > 1024; })
+ ->select(function($i) { return pathinfo($i); });
+```
+
+### Search for all users containing "Max 1", Skip 5 items, Take 2 items and select the property ID of each user:
+```php
+$result = Linq::from($users)
+ ->where(function (User $u) { return strstr($u->surname, "Max 1"); })
+ ->skip(5)
+ ->take(2)
+ ->select(function (User $u) { return $u->usrId; });
+```
+
+### Flatten multiple sequences into one sequence:
+```php
+$array1 = array("key" => "a", "data" => array("a1", "a2"));
+$array2 = array("key" => "b", "data" => array("b1", "b2"));
+$array3 = array("key" => "c", "data" => array("c1", "c2"));
+
+$allArrays = array($array1, $array2, $array3);
+
+$result = Linq::from($allArrays)
+ ->selectMany(function($x) { return $x["data"]; })
+ ->toArray();
+
+// $result is now: array("a1", "a2", "b1", "b2", "c1", "c2");
+
+```
+### Map sequence to array with key/value selectors:
+```php
+$category1 = new stdClass(); $category1->key = 1; $category1->value = "Cars";
+$category2 = new stdClass(); $category2->key = 2; $category2->value = "Ships";
+
+$result = Linq::from(array($category1, $category2))
+ ->toArray(
+ function($x) { return $x->key; }, // key-selector
+ function($x) { return $x->value; } // value-selector
+ );
+
+// $result is now: array(1 => "Cars", 2 => "Ships");
+```
+
+### The aggregate method makes it simple to perform a calculation over a sequence of values:
+```php
+$numbers = Linq::from(array(1,2,3,4));
+$sum = $numbers->aggregate(function($a, $b) { return $a + $b; });
+// echo $sum; // output: 10 (1+2+3+4)
+
+$chars = Linq::from(array("a", "b", "c"));
+$csv = $chars->aggregate(function($a, $b) { return $a . "," . $b; });
+// echo $csv; // output: "a,b,c"
+
+$chars = Linq::from(array("a", "b", "c"));
+$csv = $chars->aggregate(function($a, $b) { return $a . "," . $b; }, "seed");
+// echo $csv; // output: "seed,a,b,c"
+
+```
+
+
+### The chunk method makes it simple to split a sequence into chunks of a given size:
+```php
+$chunks = Linq::from(array("a","b","c","d","e"))->chunk(2);
+$i = 0;
+foreach($chunk in $chunks) {
+ $i++;
+ echo "Row $i <br>";
+ foreach($char in $chunk) {
+ echo $char . "|";
+ }
+}
+// Result:
+// Row 1
+// a|b
+// Row 2
+// c|d
+// Row 3
+// e|
+
+```
+
+## List of methods provided by fusonic/linq:
+
+```php
+aggregate($func, $seed = null) // Applies an accumulator function over a sequence.
+all($func) // Determines wheter all elements satisfy a condition.
+any($func) // Determines wheter any element satisfies a condition.
+average($func = null) // Computes the average of all numeric values.
+concat($second) // Concatenates 2 sequences
+contains($value) // Determines whether a sequence contains a specified element.
+count() // Counts the elements of the sequence.
+chunk($chunksize) // Splits the sequence in chunks according to $chunksize.
+except($second) // Returns all items except the ones of the given sequence.
+distinct($func = null) // Returns all distinct items of a sequence using the optional selector.
+each($func) // Performs the specified action on each element of the sequence.
+elementAt($index) // Returns the element at a specified index or throws an exception.
+elementAtOrNull($index) // Returns the element at a specified index or returns null
+first($func = null) // Returns the first element that satisfies a specified condition or throws an exception.
+firstOrNull($func = null) // Returns the first element, or NULL if the sequence contains no elements.
+groupBy($keySelector) // Groups the object according to the $keySelector generated key.
+intersect($second) // Intersects the Linq sequence with second Iterable sequence.
+last($func = null) // Returns the last element that satisfies a specified condition or throws an exception.
+lastOrNull($func = null) // Returns the last element that satisfies a condition or NULL if no such element is found.
+max($func = null) // Returns the maximum item value according to $func.
+min($func = null) // Returns the minimum item value according to $func
+orderBy($func) // Sorts the elements in ascending order according to a key provided by $func.
+orderByDescending($func) // Sorts the elements in descending order according to a key provided by $func.
+select($func) // Projects each element into a new form by invoking the selector function.
+selectMany($func) // Projects each element of a sequence to a new Linq and flattens the resulting sequences into one sequence.
+single($func = null) // Returns the only element that satisfies a specified condition or throws an exception.
+singleOrDefault($func = null) // Returns the only element that satisfies a specified condition or returns Null.
+skip($count) // Bypasses a specified number of elements and then returns the remaining elements.
+sum($func = null) // Gets the sum of all items or by invoking a transform function on each item to get a numeric value.
+take($count) // Returns a specified number of contiguous elements from the start of a sequence.
+toArray($keySelector=null, $valueSelector=null) // Creates an Array from this Linq object with an optional key selector.
+where($func) // Filters the Linq object according to func return result.
+```
+
+## Simple, Consistent and Predictable
+
+One important design goal was the principle of the least surprise. As PHP is a fully dynamic language with nearly no type-safety, it is common to shoot yourself into the foot because of accidentally mixing up incompatible types.
+
+We protect you from these programing errors by asserting that every callback functions you supply to the library must return a correctly typed value. In addition, every supported aggregate function will throw an exception if you are accidentally mixing up incompatible types.
+
+This means that we made this library totally predictable in what it does, and verified that every function has its defined exceptions which are thrown when certain operations fail, or if certain types are not correct.
+
+```php
+/* Throws an UnexpectedValueException if the
+provided callback function does not return a boolean */
+Linq::from(array("1", "1"))
+->where(function($x) { return "NOT A BOOLEAN"; });
+
+/* Throws an UnexpectedValueException if one of the values
+is not convertible to a numeric value:*/
+Linq::from(array(1, 2, "Not a numeric value"))
+->sum();
+```
+
+## Running tests
+
+You can run the test suite with the following command:
+
+```bash
+phpunit --bootstrap tests/bootstrap.php .
+```
+
diff --git a/fusonic/linq/composer.json b/fusonic/linq/composer.json
new file mode 100644
index 00000000..0b7e84e7
--- /dev/null
+++ b/fusonic/linq/composer.json
@@ -0,0 +1,22 @@
+{
+ "name": "fusonic/linq",
+ "description": "LINQ 2 objects class for PHP",
+ "keywords": ["linq", "linq2objects"],
+ "type": "library",
+ "homepage": "http://fusonic.github.io/fusonic-linq/",
+ "license": "MIT",
+ "authors": [
+ {
+ "name": "Fusonic",
+ "homepage": "http://www.fusonic.net"
+ }
+ ],
+ "require": {
+ "php": ">=5.3.2"
+ },
+ "autoload": {
+ "psr-0": {
+ "Fusonic\\Linq": "src/"
+ }
+ }
+}
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/.gitattributes b/fusonic/opengraph/.gitattributes
new file mode 100644
index 00000000..18e14aad
--- /dev/null
+++ b/fusonic/opengraph/.gitattributes
@@ -0,0 +1 @@
+/tests export-ignore
diff --git a/fusonic/opengraph/.gitignore b/fusonic/opengraph/.gitignore
new file mode 100644
index 00000000..2942141e
--- /dev/null
+++ b/fusonic/opengraph/.gitignore
@@ -0,0 +1,3 @@
+.idea/
+composer.lock
+vendor/
diff --git a/fusonic/opengraph/.scrutinizer.yml b/fusonic/opengraph/.scrutinizer.yml
new file mode 100644
index 00000000..30a9fcfa
--- /dev/null
+++ b/fusonic/opengraph/.scrutinizer.yml
@@ -0,0 +1,17 @@
+build:
+ environment:
+ php:
+ version: 7.4
+ tests:
+ override:
+ -
+ command: 'vendor/bin/phpunit --coverage-clover coverage-report'
+ coverage:
+ file: 'coverage-report'
+ format: 'clover'
+
+tools:
+ php_code_sniffer:
+ enabled: true
+ config:
+ standard: PSR2
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/README.md b/fusonic/opengraph/README.md
new file mode 100644
index 00000000..f778de81
--- /dev/null
+++ b/fusonic/opengraph/README.md
@@ -0,0 +1,142 @@
+# fusonic/opengraph
+
+[![Latest Stable Version](https://poser.pugx.org/fusonic/opengraph/v/stable)](https://packagist.org/packages/fusonic/opengraph)
+[![Total Downloads](https://poser.pugx.org/fusonic/opengraph/downloads)](https://packagist.org/packages/fusonic/opengraph)
+[![Build Status](https://api.travis-ci.org/fusonic/opengraph.svg)](https://travis-ci.org/fusonic/opengraph)
+[![License](https://poser.pugx.org/fusonic/opengraph/license)](https://packagist.org/packages/fusonic/opengraph)
+
+A simple library to read Open Graph data from the web and generate HTML code to publish your own Open Graph objects. A fallback mode enables you to read data from websites that do not implement the Open Graph protocol.
+
+Using this library you can easily retrieve stuff like meta data, video information from YouTube or Vimeo or image information from Flickr without using site-specific APIs since they all implement the Open Graph protocol.
+
+See [ogp.me](http://ogp.me) for information on the Open Graph protocol.
+
+## Requirements
+
+* PHP 7.4+
+* [fusonic/linq](https://github.com/fusonic/linq)
+* [symfony/css-selector](https://github.com/symfony/CssSelector)
+* [symfony/dom-crawler](https://github.com/symfony/DomCrawler)
+* [psr/http-client](https://github.com/php-fig/http-client), [psr/http-factory](https://github.com/php-fig/http-factory) and compatible implementation such as [guzzle/guzzle](https://github.com/guzzle/guzzle)
+
+## Installation
+
+The most flexible installation method is using Composer:
+
+``` bash
+composer require fusonic/opengraph
+```
+
+Install composer and run install command:
+``` bash
+curl -s http://getcomposer.org/installer | php
+php composer.phar install
+```
+
+Once installed, include vendor/autoload.php in your script.
+
+``` php
+require "vendor/autoload.php";
+```
+
+## Usage
+
+### Retrieve Open Graph data from a URL
+
+``` php
+use Fusonic\OpenGraph\Consumer;
+
+$consumer = new Consumer($httpClient, $httpRequestFactory);
+$object = $consumer->loadUrl("http://www.youtube.com/watch?v=P422jZg50X4");
+
+// Basic information of the object
+echo "Title: " . $object->title; // Getting started with Facebook Open Graph
+echo "Site name: " . $object->siteName; // YouTube
+echo "Description: " . $object->description; // Originally recorded at the Facebook World ...
+echo "Canonical URL: " . $object->url; // http://www.youtube.com/watch?v=P422jZg50X4
+
+// Images
+$image = $object->images[0];
+echo "Image[0] URL: " . $image->url // https://i1.ytimg.com/vi/P422jZg50X4/maxresdefault.jpg
+echo "Image[0] height: " . $image->height // null (May return height in pixels on other pages)
+echo "Image[0] width: " . $image->width // null (May return width in pixels on other pages)
+
+// Videos
+$video = $object->videos[0];
+echo "Video URL: " . $video->url // http://www.youtube.com/v/P422jZg50X4?version=3&autohide=1
+echo "Video height: " . $video->height // 1080
+echo "Video width: " . $video->width // 1920
+echo "Video type: " . $video->type // application/x-shockwave-flash
+```
+
+_There are some more properties but these are the basic and most commonly used ones._
+
+### Publish own Open Graph data
+
+``` php
+use Fusonic\OpenGraph\Elements\Image;
+use Fusonic\OpenGraph\Elements\Video;
+use Fusonic\OpenGraph\Publisher;
+use Fusonic\OpenGraph\Objects\Website;
+
+$publisher = new Publisher();
+$object = new Website();
+
+// Basic information of the object
+$object->title = "Getting started with Facebook Open Graph";
+$object->siteName = "YouTube";
+$object->description = "Originally recorded at the Facebook World ..."
+$object->url = "http://www.youtube.com/watch?v=P422jZg50X4";
+
+// Images
+$image = new Image("https://i1.ytimg.com/vi/P422jZg50X4/maxresdefault.jpg");
+$object->images[] = $image;
+
+// Videos
+$video = new Video("http://www.youtube.com/v/P422jZg50X4?version=3&autohide=1");
+$video->height = 1080;
+$video->width = 1920;
+$video->type = "application/x-shockwave-flash";
+$object->videos[] = $video;
+
+// Generate HTML code
+echo $publisher->generateHtml($object);
+// <meta property="og:description"
+// content="Originally recorded at the Facebook World ...">
+// <meta property="og:image:url"
+// content="https://i1.ytimg.com/vi/P422jZg50X4/maxresdefault.jpg">
+// <meta property="og:site_name"
+// content="YouTube">
+// <meta property="og:type"
+// content="website">
+// <meta property="og:url"
+// content="http://www.youtube.com/watch?v=P422jZg50X4">
+// <meta property="og:video:url"
+// content="http://www.youtube.com/v/P422jZg50X4?version=3&amp;autohide=1">
+// <meta property="og:video:height"
+// content="1080">
+// <meta property="og:video:type"
+// content="application/x-shockwave-flash">
+// <meta property="og:video:width"
+// content="1920">
+```
+
+_HTML code is formatted just for displaying purposes. You may choose between HTML5/XHTML output using the ```$publisher->doctype``` property._
+
+## Running tests
+
+You can run the test suite by running `phpunit` from the command line.
+
+## FAQ
+
+**I don't get any information from a webpage, but Facebook shows information for the same URL. What do I do wrong?**
+
+It seems that some pages (like Twitter) only publish OpenGraph information if Facebook's user agent string `facebookexternalhit/1.1` is used (see #28). So you should configure your PSR-18 client to use this user agent string:
+
+```php
+$client = new Psr18Client(new NativeHttpClient([ "headers" => [ "User-Agent" => "facebookexternalhit/1.1" ] ]));
+```
+
+## License
+
+This library is licensed under the MIT license.
diff --git a/fusonic/opengraph/composer.json b/fusonic/opengraph/composer.json
new file mode 100644
index 00000000..fae81442
--- /dev/null
+++ b/fusonic/opengraph/composer.json
@@ -0,0 +1,42 @@
+{
+ "name": "fusonic/opengraph",
+ "description": "PHP library for consuming and publishing Open Graph resources.",
+ "keywords": ["opengraph"],
+ "type": "library",
+ "homepage": "https://github.com/fusonic/fusonic-opengraph",
+ "license": "MIT",
+ "authors": [
+ {
+ "name": "Fusonic",
+ "homepage": "https://www.fusonic.net"
+ }
+ ],
+ "autoload": {
+ "psr-4": {
+ "Fusonic\\OpenGraph\\": "src/"
+ }
+ },
+ "autoload-dev": {
+ "psr-4": {
+ "Fusonic\\OpenGraph\\Test\\": "tests/"
+ }
+ },
+ "require": {
+ "php": "^7.4|^8.0",
+ "ext-dom": "*",
+ "symfony/dom-crawler": "^3.0|^4.0|^5.0|^6.0",
+ "symfony/css-selector": "^3.0|^4.0|^5.0|^6.0",
+ "fusonic/linq": "^1.0",
+ "psr/http-client": "^1.0",
+ "psr/http-factory": "^1.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.0",
+ "symfony/http-client": "^6.0",
+ "nyholm/psr7": "^1.2"
+ },
+ "suggest": {
+ "symfony/http-client": "^5.0",
+ "nyholm/psr7": "^1.2"
+ }
+}
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"
+}