File manager - Edit - /home/buyherba/public_html/video/lib.tar
Back
classes/symfony/polyfill-php80/Resources/stubs/Attribute.php 0000644 00000001404 15217646066 0020276 0 ustar 00 <?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. */ #[WC_Vendor_Attribute(Attribute::TARGET_CLASS)] final class WC_Vendor_Attribute { public const TARGET_CLASS = 1; public const TARGET_FUNCTION = 2; public const TARGET_METHOD = 4; public const TARGET_PROPERTY = 8; public const TARGET_CLASS_CONSTANT = 16; public const TARGET_PARAMETER = 32; public const TARGET_ALL = 63; public const IS_REPEATABLE = 64; /** @var int */ public $flags; public function __construct(int $flags = self::TARGET_ALL) { $this->flags = $flags; } } classes/symfony/polyfill-php80/Resources/stubs/Stringable.php 0000644 00000000626 15217646073 0020430 0 ustar 00 <?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. */ if (\PHP_VERSION_ID < 80000) { interface WC_Vendor_Stringable { /** * @return string */ public function __toString(); } } classes/symfony/polyfill-php80/Resources/stubs/ValueError.php 0000644 00000000510 15217646100 0020403 0 ustar 00 <?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. */ if (\PHP_VERSION_ID < 80000) { class WC_Vendor_ValueError extends Error { } } classes/symfony/polyfill-php80/Resources/stubs/UnhandledMatchError.php 0000644 00000000521 15217646105 0022215 0 ustar 00 <?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. */ if (\PHP_VERSION_ID < 80000) { class WC_Vendor_UnhandledMatchError extends Error { } } classes/symfony/polyfill-php80/Resources/stubs/PhpToken.php 0000644 00000000613 15217646112 0020054 0 ustar 00 <?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. */ if (\PHP_VERSION_ID < 80000 && extension_loaded('tokenizer')) { class WC_Vendor_PhpToken extends Symfony\Polyfill\Php80\WC_Vendor_PhpToken { } } packages/Psr/Container/ContainerInterface.php 0000644 00000002056 15217646117 0015301 0 ustar 00 <?php declare(strict_types=1); namespace Automattic\WooCommerce\Vendor\Psr\Container; /** * Describes the interface of a container that exposes methods to read its entries. */ interface ContainerInterface { /** * Finds an entry of the container by its identifier and returns it. * * @param string $id Identifier of the entry to look for. * * @throws NotFoundExceptionInterface No entry was found for **this** identifier. * @throws ContainerExceptionInterface Error while retrieving the entry. * * @return mixed Entry. */ public function get(string $id); /** * Returns true if the container can return an entry for the given identifier. * Returns false otherwise. * * `has($id)` returning true does not mean that `get($id)` will not throw an exception. * It does however mean that `get($id)` will not throw a `NotFoundExceptionInterface`. * * @param string $id Identifier of the entry to look for. * * @return bool */ public function has(string $id); } packages/Psr/Container/NotFoundExceptionInterface.php 0000644 00000000274 15217646125 0016771 0 ustar 00 <?php namespace Automattic\WooCommerce\Vendor\Psr\Container; /** * No entry was found in the container. */ interface NotFoundExceptionInterface extends ContainerExceptionInterface { } packages/Psr/Container/ContainerExceptionInterface.php 0000644 00000000326 15217646132 0017153 0 ustar 00 <?php namespace Automattic\WooCommerce\Vendor\Psr\Container; use Throwable; /** * Base interface representing a generic exception in a container. */ interface ContainerExceptionInterface extends Throwable { } packages/Symfony/Component/CssSelector/CssSelectorConverter.php 0000644 00000004473 15217646137 0021037 0 ustar 00 <?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 Automattic\WooCommerce\Vendor\Symfony\Component\CssSelector; use Automattic\WooCommerce\Vendor\Symfony\Component\CssSelector\Parser\Shortcut\ClassParser; use Automattic\WooCommerce\Vendor\Symfony\Component\CssSelector\Parser\Shortcut\ElementParser; use Automattic\WooCommerce\Vendor\Symfony\Component\CssSelector\Parser\Shortcut\EmptyStringParser; use Automattic\WooCommerce\Vendor\Symfony\Component\CssSelector\Parser\Shortcut\HashParser; use Automattic\WooCommerce\Vendor\Symfony\Component\CssSelector\XPath\Extension\HtmlExtension; use Automattic\WooCommerce\Vendor\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); } } packages/Symfony/Component/CssSelector/composer.json 0000644 00000001530 15217646144 0016714 0 ustar 00 { "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" } packages/Symfony/Component/CssSelector/Parser/Handler/HandlerInterface.php 0000644 00000001542 15217646152 0022714 0 ustar 00 <?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 Automattic\WooCommerce\Vendor\Symfony\Component\CssSelector\Parser\Handler; use Automattic\WooCommerce\Vendor\Symfony\Component\CssSelector\Parser\Reader; use Automattic\WooCommerce\Vendor\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; } packages/Symfony/Component/CssSelector/Parser/Handler/NumberHandler.php 0000644 00000003010 15217646157 0022241 0 ustar 00 <?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 Automattic\WooCommerce\Vendor\Symfony\Component\CssSelector\Parser\Handler; use Automattic\WooCommerce\Vendor\Symfony\Component\CssSelector\Parser\Reader; use Automattic\WooCommerce\Vendor\Symfony\Component\CssSelector\Parser\Token; use Automattic\WooCommerce\Vendor\Symfony\Component\CssSelector\Parser\Tokenizer\TokenizerPatterns; use Automattic\WooCommerce\Vendor\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; } } packages/Symfony/Component/CssSelector/Parser/Handler/CommentHandler.php 0000644 00000002314 15217646164 0022417 0 ustar 00 <?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 Automattic\WooCommerce\Vendor\Symfony\Component\CssSelector\Parser\Handler; use Automattic\WooCommerce\Vendor\Symfony\Component\CssSelector\Parser\Reader; use Automattic\WooCommerce\Vendor\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; } } packages/Symfony/Component/CssSelector/Parser/Handler/StringHandler.php 0000644 00000005167 15217646171 0022272 0 ustar 00 <?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 Automattic\WooCommerce\Vendor\Symfony\Component\CssSelector\Parser\Handler; use Automattic\WooCommerce\Vendor\Symfony\Component\CssSelector\Exception\InternalErrorException; use Automattic\WooCommerce\Vendor\Symfony\Component\CssSelector\Exception\SyntaxErrorException; use Automattic\WooCommerce\Vendor\Symfony\Component\CssSelector\Parser\Reader; use Automattic\WooCommerce\Vendor\Symfony\Component\CssSelector\Parser\Token; use Automattic\WooCommerce\Vendor\Symfony\Component\CssSelector\Parser\Tokenizer\TokenizerEscaping; use Automattic\WooCommerce\Vendor\Symfony\Component\CssSelector\Parser\Tokenizer\TokenizerPatterns; use Automattic\WooCommerce\Vendor\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; } } packages/Symfony/Component/CssSelector/Parser/Handler/IdentifierHandler.php 0000644 00000003412 15217646176 0023102 0 ustar 00 <?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 Automattic\WooCommerce\Vendor\Symfony\Component\CssSelector\Parser\Handler; use Automattic\WooCommerce\Vendor\Symfony\Component\CssSelector\Parser\Reader; use Automattic\WooCommerce\Vendor\Symfony\Component\CssSelector\Parser\Token; use Automattic\WooCommerce\Vendor\Symfony\Component\CssSelector\Parser\Tokenizer\TokenizerEscaping; use Automattic\WooCommerce\Vendor\Symfony\Component\CssSelector\Parser\Tokenizer\TokenizerPatterns; use Automattic\WooCommerce\Vendor\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; } } packages/Symfony/Component/CssSelector/Parser/Handler/HashHandler.php 0000644 00000003370 15217646176 0021706 0 ustar 00 <?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 Automattic\WooCommerce\Vendor\Symfony\Component\CssSelector\Parser\Handler; use Automattic\WooCommerce\Vendor\Symfony\Component\CssSelector\Parser\Reader; use Automattic\WooCommerce\Vendor\Symfony\Component\CssSelector\Parser\Token; use Automattic\WooCommerce\Vendor\Symfony\Component\CssSelector\Parser\Tokenizer\TokenizerEscaping; use Automattic\WooCommerce\Vendor\Symfony\Component\CssSelector\Parser\Tokenizer\TokenizerPatterns; use Automattic\WooCommerce\Vendor\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; } } packages/Symfony/Component/CssSelector/Parser/Handler/WhitespaceHandler.php 0000644 00000002437 15217646203 0023111 0 ustar 00 <?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 Automattic\WooCommerce\Vendor\Symfony\Component\CssSelector\Parser\Handler; use Automattic\WooCommerce\Vendor\Symfony\Component\CssSelector\Parser\Reader; use Automattic\WooCommerce\Vendor\Symfony\Component\CssSelector\Parser\Token; use Automattic\WooCommerce\Vendor\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; } } packages/Symfony/Component/CssSelector/Parser/Token.php 0000644 00000004752 15217646210 0017222 0 ustar 00 <?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 Automattic\WooCommerce\Vendor\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); } } packages/Symfony/Component/CssSelector/Parser/Tokenizer/Tokenizer.php 0000644 00000004210 15217646215 0022060 0 ustar 00 <?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 Automattic\WooCommerce\Vendor\Symfony\Component\CssSelector\Parser\Tokenizer; use Automattic\WooCommerce\Vendor\Symfony\Component\CssSelector\Parser\Handler; use Automattic\WooCommerce\Vendor\Symfony\Component\CssSelector\Parser\Reader; use Automattic\WooCommerce\Vendor\Symfony\Component\CssSelector\Parser\Token; use Automattic\WooCommerce\Vendor\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(); } } packages/Symfony/Component/CssSelector/Parser/Tokenizer/TokenizerPatterns.php 0000644 00000005370 15217646223 0023610 0 ustar 00 <?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 Automattic\WooCommerce\Vendor\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'; } } packages/Symfony/Component/CssSelector/Parser/Tokenizer/TokenizerEscaping.php 0000644 00000003416 15217646230 0023536 0 ustar 00 <?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 Automattic\WooCommerce\Vendor\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); } } packages/Symfony/Component/CssSelector/Parser/ParserInterface.php 0000644 00000001545 15217646230 0021216 0 ustar 00 <?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 Automattic\WooCommerce\Vendor\Symfony\Component\CssSelector\Parser; use Automattic\WooCommerce\Vendor\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; } packages/Symfony/Component/CssSelector/Parser/Reader.php 0000644 00000003570 15217646235 0017350 0 ustar 00 <?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 Automattic\WooCommerce\Vendor\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; } } packages/Symfony/Component/CssSelector/Parser/TokenStream.php 0000644 00000006622 15217646242 0020401 0 ustar 00 <?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 Automattic\WooCommerce\Vendor\Symfony\Component\CssSelector\Parser; use Automattic\WooCommerce\Vendor\Symfony\Component\CssSelector\Exception\InternalErrorException; use Automattic\WooCommerce\Vendor\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(); } } } packages/Symfony/Component/CssSelector/Parser/Shortcut/EmptyStringParser.php 0000644 00000002506 15217646247 0023424 0 ustar 00 <?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 Automattic\WooCommerce\Vendor\Symfony\Component\CssSelector\Parser\Shortcut; use Automattic\WooCommerce\Vendor\Symfony\Component\CssSelector\Node\ElementNode; use Automattic\WooCommerce\Vendor\Symfony\Component\CssSelector\Node\SelectorNode; use Automattic\WooCommerce\Vendor\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 []; } } packages/Symfony/Component/CssSelector/Parser/Shortcut/HashParser.php 0000644 00000003312 15217646254 0022014 0 ustar 00 <?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 Automattic\WooCommerce\Vendor\Symfony\Component\CssSelector\Parser\Shortcut; use Automattic\WooCommerce\Vendor\Symfony\Component\CssSelector\Node\ElementNode; use Automattic\WooCommerce\Vendor\Symfony\Component\CssSelector\Node\HashNode; use Automattic\WooCommerce\Vendor\Symfony\Component\CssSelector\Node\SelectorNode; use Automattic\WooCommerce\Vendor\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 []; } } packages/Symfony/Component/CssSelector/Parser/Shortcut/ClassParser.php 0000644 00000003322 15217646261 0022175 0 ustar 00 <?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 Automattic\WooCommerce\Vendor\Symfony\Component\CssSelector\Parser\Shortcut; use Automattic\WooCommerce\Vendor\Symfony\Component\CssSelector\Node\ClassNode; use Automattic\WooCommerce\Vendor\Symfony\Component\CssSelector\Node\ElementNode; use Automattic\WooCommerce\Vendor\Symfony\Component\CssSelector\Node\SelectorNode; use Automattic\WooCommerce\Vendor\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 []; } } packages/Symfony/Component/CssSelector/Parser/Shortcut/ElementParser.php 0000644 00000002744 15217646266 0022535 0 ustar 00 <?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 Automattic\WooCommerce\Vendor\Symfony\Component\CssSelector\Parser\Shortcut; use Automattic\WooCommerce\Vendor\Symfony\Component\CssSelector\Node\ElementNode; use Automattic\WooCommerce\Vendor\Symfony\Component\CssSelector\Node\SelectorNode; use Automattic\WooCommerce\Vendor\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 []; } } packages/Symfony/Component/CssSelector/Parser/Parser.php 0000644 00000026733 15217646273 0017412 0 ustar 00 <?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 Automattic\WooCommerce\Vendor\Symfony\Component\CssSelector\Parser; use Automattic\WooCommerce\Vendor\Symfony\Component\CssSelector\Exception\SyntaxErrorException; use Automattic\WooCommerce\Vendor\Symfony\Component\CssSelector\Node; use Automattic\WooCommerce\Vendor\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()); } } packages/Symfony/Component/CssSelector/XPath/TranslatorInterface.php 0000644 00000002067 15217646301 0021702 0 ustar 00 <?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 Automattic\WooCommerce\Vendor\Symfony\Component\CssSelector\XPath; use Automattic\WooCommerce\Vendor\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; } packages/Symfony/Component/CssSelector/XPath/XPathExpr.php 0000644 00000004726 15217646306 0017624 0 ustar 00 <?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 Automattic\WooCommerce\Vendor\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; } } packages/Symfony/Component/CssSelector/XPath/Extension/AbstractExtension.php 0000644 00000002423 15217646313 0023343 0 ustar 00 <?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 Automattic\WooCommerce\Vendor\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 []; } } packages/Symfony/Component/CssSelector/XPath/Extension/FunctionExtension.php 0000644 00000012476 15217646320 0023374 0 ustar 00 <?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 Automattic\WooCommerce\Vendor\Symfony\Component\CssSelector\XPath\Extension; use Automattic\WooCommerce\Vendor\Symfony\Component\CssSelector\Exception\ExpressionErrorException; use Automattic\WooCommerce\Vendor\Symfony\Component\CssSelector\Exception\SyntaxErrorException; use Automattic\WooCommerce\Vendor\Symfony\Component\CssSelector\Node\FunctionNode; use Automattic\WooCommerce\Vendor\Symfony\Component\CssSelector\Parser\Parser; use Automattic\WooCommerce\Vendor\Symfony\Component\CssSelector\XPath\Translator; use Automattic\WooCommerce\Vendor\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'; } } packages/Symfony/Component/CssSelector/XPath/Extension/ExtensionInterface.php 0000644 00000003043 15217646325 0023502 0 ustar 00 <?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 Automattic\WooCommerce\Vendor\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; } packages/Symfony/Component/CssSelector/XPath/Extension/AttributeMatchingExtension.php 0000644 00000007510 15217646332 0025221 0 ustar 00 <?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 Automattic\WooCommerce\Vendor\Symfony\Component\CssSelector\XPath\Extension; use Automattic\WooCommerce\Vendor\Symfony\Component\CssSelector\XPath\Translator; use Automattic\WooCommerce\Vendor\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'; } } packages/Symfony/Component/CssSelector/XPath/Extension/CombinationExtension.php 0000644 00000003721 15217646337 0024052 0 ustar 00 <?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 Automattic\WooCommerce\Vendor\Symfony\Component\CssSelector\XPath\Extension; use Automattic\WooCommerce\Vendor\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'; } } packages/Symfony/Component/CssSelector/XPath/Extension/PseudoClassExtension.php 0000644 00000006657 15217646344 0024046 0 ustar 00 <?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 Automattic\WooCommerce\Vendor\Symfony\Component\CssSelector\XPath\Extension; use Automattic\WooCommerce\Vendor\Symfony\Component\CssSelector\Exception\ExpressionErrorException; use Automattic\WooCommerce\Vendor\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'; } } packages/Symfony/Component/CssSelector/XPath/Extension/NodeExtension.php 0000644 00000013575 15217646352 0022502 0 ustar 00 <?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 Automattic\WooCommerce\Vendor\Symfony\Component\CssSelector\XPath\Extension; use Automattic\WooCommerce\Vendor\Symfony\Component\CssSelector\Node; use Automattic\WooCommerce\Vendor\Symfony\Component\CssSelector\XPath\Translator; use Automattic\WooCommerce\Vendor\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'], 'WC_Vendor_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); } } packages/Symfony/Component/CssSelector/XPath/Extension/HtmlExtension.php 0000644 00000013654 15217646357 0022524 0 ustar 00 <?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 Automattic\WooCommerce\Vendor\Symfony\Component\CssSelector\XPath\Extension; use Automattic\WooCommerce\Vendor\Symfony\Component\CssSelector\Exception\ExpressionErrorException; use Automattic\WooCommerce\Vendor\Symfony\Component\CssSelector\Node\FunctionNode; use Automattic\WooCommerce\Vendor\Symfony\Component\CssSelector\XPath\Translator; use Automattic\WooCommerce\Vendor\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'; } } packages/Symfony/Component/CssSelector/XPath/Translator.php 0000644 00000016574 15217646364 0020102 0 ustar 00 <?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 Automattic\WooCommerce\Vendor\Symfony\Component\CssSelector\XPath; use Automattic\WooCommerce\Vendor\Symfony\Component\CssSelector\Exception\ExpressionErrorException; use Automattic\WooCommerce\Vendor\Symfony\Component\CssSelector\Node\FunctionNode; use Automattic\WooCommerce\Vendor\Symfony\Component\CssSelector\Node\NodeInterface; use Automattic\WooCommerce\Vendor\Symfony\Component\CssSelector\Node\SelectorNode; use Automattic\WooCommerce\Vendor\Symfony\Component\CssSelector\Parser\Parser; use Automattic\WooCommerce\Vendor\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('WC_Vendor_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); } } packages/Symfony/Component/CssSelector/Node/SelectorNode.php 0000644 00000002710 15217646371 0020161 0 ustar 00 <?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 Automattic\WooCommerce\Vendor\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 : ''); } } packages/Symfony/Component/CssSelector/Node/HashNode.php 0000644 00000002437 15217646376 0017277 0 ustar 00 <?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 Automattic\WooCommerce\Vendor\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); } } packages/Symfony/Component/CssSelector/Node/NegationNode.php 0000644 00000002616 15217646403 0020146 0 ustar 00 <?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 Automattic\WooCommerce\Vendor\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); } } packages/Symfony/Component/CssSelector/Node/PseudoNode.php 0000644 00000002555 15217646410 0017641 0 ustar 00 <?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 Automattic\WooCommerce\Vendor\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); } } packages/Symfony/Component/CssSelector/Node/FunctionNode.php 0000644 00000003541 15217646415 0020170 0 ustar 00 <?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 Automattic\WooCommerce\Vendor\Symfony\Component\CssSelector\Node; use Automattic\WooCommerce\Vendor\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.']' : ''); } } packages/Symfony/Component/CssSelector/Node/AbstractNode.php 0000644 00000001641 15217646423 0020144 0 ustar 00 <?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 Automattic\WooCommerce\Vendor\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; } } packages/Symfony/Component/CssSelector/Node/ClassNode.php 0000644 00000002460 15217646430 0017444 0 ustar 00 <?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 Automattic\WooCommerce\Vendor\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); } } packages/Symfony/Component/CssSelector/Node/AttributeNode.php 0000644 00000004152 15217646435 0020347 0 ustar 00 <?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 Automattic\WooCommerce\Vendor\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); } } packages/Symfony/Component/CssSelector/Node/NodeInterface.php 0000644 00000001351 15217646442 0020300 0 ustar 00 <?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 Automattic\WooCommerce\Vendor\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; } packages/Symfony/Component/CssSelector/Node/ElementNode.php 0000644 00000002605 15217646447 0020001 0 ustar 00 <?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 Automattic\WooCommerce\Vendor\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); } } packages/Symfony/Component/CssSelector/Node/Specificity.php 0000644 00000003452 15217646454 0020054 0 ustar 00 <?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 Automattic\WooCommerce\Vendor\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; } } packages/Symfony/Component/CssSelector/Node/CombinedSelectorNode.php 0000644 00000003221 15217646461 0021620 0 ustar 00 <?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 Automattic\WooCommerce\Vendor\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); } } packages/Symfony/Component/CssSelector/LICENSE 0000644 00000002054 15217646466 0015210 0 ustar 00 Copyright (c) 2004-present 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. packages/Symfony/Component/CssSelector/Exception/ExpressionErrorException.php 0000644 00000001237 15217646473 0023702 0 ustar 00 <?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 Automattic\WooCommerce\Vendor\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 { } packages/Symfony/Component/CssSelector/Exception/ParseException.php 0000644 00000001234 15217646501 0021570 0 ustar 00 <?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 Automattic\WooCommerce\Vendor\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 { } packages/Symfony/Component/CssSelector/Exception/InternalErrorException.php 0000644 00000001235 15217646506 0023312 0 ustar 00 <?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 Automattic\WooCommerce\Vendor\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 { } packages/Symfony/Component/CssSelector/Exception/ExceptionInterface.php 0000644 00000001161 15217646513 0022420 0 ustar 00 <?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 Automattic\WooCommerce\Vendor\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 { } packages/Symfony/Component/CssSelector/Exception/SyntaxErrorException.php 0000644 00000003300 15217646520 0023013 0 ustar 00 <?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 Automattic\WooCommerce\Vendor\Symfony\Component\CssSelector\Exception; use Automattic\WooCommerce\Vendor\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.'); } } packages/Symfony/Polyfill/Php80/Resources/stubs/Attribute.php 0000644 00000001360 15217646525 0020262 0 ustar 00 <?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. */ #[Attribute(Attribute::TARGET_CLASS)] final class Attribute { public const TARGET_CLASS = 1; public const TARGET_FUNCTION = 2; public const TARGET_METHOD = 4; public const TARGET_PROPERTY = 8; public const TARGET_CLASS_CONSTANT = 16; public const TARGET_PARAMETER = 32; public const TARGET_ALL = 63; public const IS_REPEATABLE = 64; /** @var int */ public $flags; public function __construct(int $flags = self::TARGET_ALL) { $this->flags = $flags; } } packages/Symfony/Polyfill/Php80/Resources/stubs/Stringable.php 0000644 00000000614 15217646532 0020410 0 ustar 00 <?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. */ if (\PHP_VERSION_ID < 80000) { interface Stringable { /** * @return string */ public function __toString(); } } packages/Symfony/Polyfill/Php80/Resources/stubs/ValueError.php 0000644 00000000476 15217646537 0020417 0 ustar 00 <?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. */ if (\PHP_VERSION_ID < 80000) { class ValueError extends Error { } } packages/Symfony/Polyfill/Php80/Resources/stubs/UnhandledMatchError.php 0000644 00000000507 15217646544 0022213 0 ustar 00 <?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. */ if (\PHP_VERSION_ID < 80000) { class UnhandledMatchError extends Error { } } packages/Symfony/Polyfill/Php80/Resources/stubs/PhpToken.php 0000644 00000000625 15217646552 0020052 0 ustar 00 <?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. */ if (\PHP_VERSION_ID < 80000 && extension_loaded('tokenizer')) { class PhpToken extends Automattic\WooCommerce\Vendor\Symfony\Polyfill\Php80\PhpToken { } } packages/Symfony/Polyfill/Php80/Php80.php 0000644 00000007027 15217646557 0014117 0 ustar 00 <?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 Automattic\WooCommerce\Vendor\Symfony\Polyfill\Php80; /** * @author Ion Bazan <ion.bazan@gmail.com> * @author Nico Oelgart <nicoswd@gmail.com> * @author Nicolas Grekas <p@tchwork.com> * * @internal */ final class Php80 { public static function fdiv(float $dividend, float $divisor): float { return @($dividend / $divisor); } public static function get_debug_type($value): string { switch (true) { case null === $value: return 'null'; case \is_bool($value): return 'bool'; case \is_string($value): return 'string'; case \is_array($value): return 'array'; case \is_int($value): return 'int'; case \is_float($value): return 'float'; case \is_object($value): break; case $value instanceof \__PHP_Incomplete_Class: return '__PHP_Incomplete_Class'; default: if (null === $type = @get_resource_type($value)) { return 'unknown'; } if ('Unknown' === $type) { $type = 'closed'; } return "resource ($type)"; } $class = \get_class($value); if (false === strpos($class, '@')) { return $class; } return (get_parent_class($class) ?: key(class_implements($class)) ?: 'class').'@anonymous'; } public static function get_resource_id($res): int { if (!\is_resource($res) && null === @get_resource_type($res)) { throw new \TypeError(sprintf('Argument 1 passed to get_resource_id() must be of the type resource, %s given', get_debug_type($res))); } return (int) $res; } public static function preg_last_error_msg(): string { switch (preg_last_error()) { case \PREG_INTERNAL_ERROR: return 'Internal error'; case \PREG_BAD_UTF8_ERROR: return 'Malformed UTF-8 characters, possibly incorrectly encoded'; case \PREG_BAD_UTF8_OFFSET_ERROR: return 'The offset did not correspond to the beginning of a valid UTF-8 code point'; case \PREG_BACKTRACK_LIMIT_ERROR: return 'Backtrack limit exhausted'; case \PREG_RECURSION_LIMIT_ERROR: return 'Recursion limit exhausted'; case \PREG_JIT_STACKLIMIT_ERROR: return 'JIT stack limit exhausted'; case \PREG_NO_ERROR: return 'No error'; default: return 'Unknown error'; } } public static function str_contains(string $haystack, string $needle): bool { return '' === $needle || false !== strpos($haystack, $needle); } public static function str_starts_with(string $haystack, string $needle): bool { return 0 === strncmp($haystack, $needle, \strlen($needle)); } public static function str_ends_with(string $haystack, string $needle): bool { if ('' === $needle || $needle === $haystack) { return true; } if ('' === $haystack) { return false; } $needleLength = \strlen($needle); return $needleLength <= \strlen($haystack) && 0 === substr_compare($haystack, $needle, -$needleLength); } } packages/Symfony/Polyfill/Php80/composer.json 0000644 00000001765 15217646564 0015232 0 ustar 00 { "name": "symfony/polyfill-php80", "type": "library", "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", "keywords": ["polyfill", "shim", "compatibility", "portable"], "homepage": "https://symfony.com", "license": "MIT", "authors": [ { "name": "Ion Bazan", "email": "ion.bazan@gmail.com" }, { "name": "Nicolas Grekas", "email": "p@tchwork.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], "require": { "php": ">=7.2" }, "autoload": { "psr-4": { "Symfony\\Polyfill\\Php80\\": "" }, "files": [ "bootstrap.php" ], "classmap": [ "Resources/stubs" ] }, "minimum-stability": "dev", "extra": { "thanks": { "name": "symfony/polyfill", "url": "https://github.com/symfony/polyfill" } } } packages/Symfony/Polyfill/Php80/bootstrap.php 0000644 00000003032 15217646571 0015221 0 ustar 00 <?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. */ use Automattic\WooCommerce\Vendor\Symfony\Polyfill\Php80 as p; if (\PHP_VERSION_ID >= 80000) { return; } if (!defined('FILTER_VALIDATE_BOOL') && defined('FILTER_VALIDATE_BOOLEAN')) { define('FILTER_VALIDATE_BOOL', \FILTER_VALIDATE_BOOLEAN); } if (!function_exists('fdiv')) { function fdiv(float $num1, float $num2): float { return p\Php80::fdiv($num1, $num2); } } if (!function_exists('preg_last_error_msg')) { function preg_last_error_msg(): string { return p\Php80::preg_last_error_msg(); } } if (!function_exists('str_contains')) { function str_contains(?string $haystack, ?string $needle): bool { return p\Php80::str_contains($haystack ?? '', $needle ?? ''); } } if (!function_exists('str_starts_with')) { function str_starts_with(?string $haystack, ?string $needle): bool { return p\Php80::str_starts_with($haystack ?? '', $needle ?? ''); } } if (!function_exists('str_ends_with')) { function str_ends_with(?string $haystack, ?string $needle): bool { return p\Php80::str_ends_with($haystack ?? '', $needle ?? ''); } } if (!function_exists('get_debug_type')) { function get_debug_type($value): string { return p\Php80::get_debug_type($value); } } if (!function_exists('get_resource_id')) { function get_resource_id($resource): int { return p\Php80::get_resource_id($resource); } } packages/Symfony/Polyfill/Php80/LICENSE 0000644 00000002054 15217646576 0013510 0 ustar 00 Copyright (c) 2020-present 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. packages/Symfony/Polyfill/Php80/PhpToken.php 0000644 00000004353 15217646576 0014750 0 ustar 00 <?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 Automattic\WooCommerce\Vendor\Symfony\Polyfill\Php80; /** * @author Fedonyuk Anton <info@ensostudio.ru> * * @internal */ class PhpToken implements \Stringable { /** * @var int */ public $id; /** * @var string */ public $text; /** * @var -1|positive-int */ public $line; /** * @var int */ public $pos; /** * @param -1|positive-int $line */ public function __construct(int $id, string $text, int $line = -1, int $position = -1) { $this->id = $id; $this->text = $text; $this->line = $line; $this->pos = $position; } public function getTokenName(): ?string { if ('UNKNOWN' === $name = token_name($this->id)) { $name = \strlen($this->text) > 1 || \ord($this->text) < 32 ? null : $this->text; } return $name; } /** * @param int|string|array $kind */ public function is($kind): bool { foreach ((array) $kind as $value) { if (\in_array($value, [$this->id, $this->text], true)) { return true; } } return false; } public function isIgnorable(): bool { return \in_array($this->id, [\T_WHITESPACE, \T_COMMENT, \T_DOC_COMMENT, \T_OPEN_TAG], true); } public function __toString(): string { return (string) $this->text; } /** * @return list<static> */ public static function tokenize(string $code, int $flags = 0): array { $line = 1; $position = 0; $tokens = token_get_all($code, $flags); foreach ($tokens as $index => $token) { if (\is_string($token)) { $id = \ord($token); $text = $token; } else { [$id, $text, $line] = $token; } $tokens[$index] = new static($id, $text, $line, $position); $position += \strlen($text); } return $tokens; } } packages/Pelago/Emogrifier/Utilities/Preg.php 0000644 00000015555 15217646603 0015227 0 ustar 00 <?php declare(strict_types=1); namespace Automattic\WooCommerce\Vendor\Pelago\Emogrifier\Utilities; /** * PHP's `preg_*` functions can return `false` on failure. * Failure is rare but may occur with a complex pattern applied to a long subject. * Catastrophic backtracking may occur ({@see https://www.regular-expressions.info/catastrophic.html}). * Failure may also occur due to programmer error, if an invalid pattern is provided. * * Catering for failure in each case clutters up the code with error handling. * This class provides wrappers for some `preg_*` functions, with errors handled either * - by throwing an exception, or * - by triggering a user error and providing fallback logic (e.g. returning the subject string unmodified). * * @internal */ final class Preg { /** * whether to throw exceptions on errors (or call `trigger_error` and implement fallback) * * @var bool */ private $throwExceptions = false; /** * Sets whether exceptions should be thrown if an error occurs. */ public function throwExceptions(bool $throw): self { $this->throwExceptions = $throw; return $this; } /** * Wraps `preg_replace`, though does not support `$subject` being an array. * If an error occurs, and exceptions are not being thrown, the original `$subject` is returned. * * @param non-empty-string|non-empty-array<non-empty-string> $pattern * @param string|non-empty-array<string> $replacement * * @throws \RuntimeException */ public function replace($pattern, $replacement, string $subject, int $limit = -1, ?int &$count = null): string { $result = \preg_replace($pattern, $replacement, $subject, $limit, $count); if ($result === null) { $this->logOrThrowPregLastError(); $result = $subject; } return $result; } /** * Wraps `preg_replace_callback`, though does not support `$subject` being an array. * If an error occurs, and exceptions are not being thrown, the original `$subject` is returned. * * Note that (unlike when calling `preg_replace_callback`), `$callback` cannot be a non-public method * represented by an array comprising an object or class name and the method name. * To circumvent that, use `\Closure::fromCallable([$objectOrClassName, 'method'])`. * * @param non-empty-string|non-empty-array<non-empty-string> $pattern * * @throws \RuntimeException */ public function replaceCallback( $pattern, callable $callback, string $subject, int $limit = -1, ?int &$count = null ): string { $result = \preg_replace_callback($pattern, $callback, $subject, $limit, $count); if ($result === null) { $this->logOrThrowPregLastError(); $result = $subject; } return $result; } /** * Wraps `preg_split`. * If an error occurs, and exceptions are not being thrown, * a single-element array containing the original `$subject` is returned. * This method does not support the `PREG_SPLIT_OFFSET_CAPTURE` flag and will throw an exception if it is specified. * * @param non-empty-string $pattern * * @return array<int, string> * * @throws \RuntimeException */ public function split(string $pattern, string $subject, int $limit = -1, int $flags = 0): array { if (($flags & PREG_SPLIT_OFFSET_CAPTURE) !== 0) { throw new \RuntimeException('PREG_SPLIT_OFFSET_CAPTURE is not supported by Preg::split', 1726506348); } $result = \preg_split($pattern, $subject, $limit, $flags); if ($result === false) { $this->logOrThrowPregLastError(); $result = [$subject]; } return $result; } /** * Wraps `preg_match`. * If an error occurs, and exceptions are not being thrown, * zero (`0`) is returned, and if the `$matches` parameter is provided, it is set to an empty array. * This method does not currently support the `$flags` or `$offset` parameters. * * @param non-empty-string $pattern * @param array<int, string> $matches * * @return 0|1 * * @throws \RuntimeException */ public function match(string $pattern, string $subject, ?array &$matches = null): int { $result = \preg_match($pattern, $subject, $matches); if ($result === false) { $this->logOrThrowPregLastError(); $result = 0; $matches = []; } return $result; } /** * Wraps `preg_match_all`. * * If an error occurs, and exceptions are not being thrown, zero (`0`) is returned. * * In the error case, if the `$matches` parameter is provided, it is set to an array containing empty arrays for the * full pattern match and any possible subpattern match that might be expected. * The algorithm to determine the length of this array simply counts the number of opening parentheses in the * `$pattern`, which may result in a longer array than expected, but guarantees that it is at least as long as * expected. * * This method does not currently support the `$flags` or `$offset` parameters. * * @param non-empty-string $pattern * @param array<int, array<int, string>> $matches * * @throws \RuntimeException */ public function matchAll(string $pattern, string $subject, ?array &$matches = null): int { $result = \preg_match_all($pattern, $subject, $matches); if ($result === false) { $this->logOrThrowPregLastError(); $result = 0; $matches = \array_fill(0, \substr_count($pattern, '(') + 1, []); } return $result; } /** * Obtains the name of the error constant for `preg_last_error` * (based on code posted at {@see https://www.php.net/manual/en/function.preg-last-error.php#124124}) * and puts it into an error message which is either passed to `trigger_error` * or used in the exception which is thrown (depending on the `$throwExceptions` property). * * @throws \RuntimeException */ private function logOrThrowPregLastError(): void { $pcreConstants = \get_defined_constants(true)['pcre']; $pcreErrorConstantNames = \array_flip(\array_filter( $pcreConstants, static function (string $key): bool { return \substr($key, -6) === '_ERROR'; }, ARRAY_FILTER_USE_KEY )); $pregLastError = \preg_last_error(); $message = 'PCRE regex execution error `' . (string) ($pcreErrorConstantNames[$pregLastError] ?? $pregLastError) . '`'; if ($this->throwExceptions) { throw new \RuntimeException($message, 1592870147); } \trigger_error($message); } } packages/Pelago/Emogrifier/Utilities/CssConcatenator.php 0000644 00000015164 15217646610 0017415 0 ustar 00 <?php declare(strict_types=1); namespace Automattic\WooCommerce\Vendor\Pelago\Emogrifier\Utilities; /** * Facilitates building a CSS string by appending rule blocks one at a time, checking whether the media query, * selectors, or declarations block are the same as those from the preceding block and combining blocks in such cases. * * Example: * $concatenator = new CssConcatenator(); * $concatenator->append(['body'], 'color: blue;'); * $concatenator->append(['body'], 'font-size: 16px;'); * $concatenator->append(['p'], 'margin: 1em 0;'); * $concatenator->append(['ul', 'ol'], 'margin: 1em 0;'); * $concatenator->append(['body'], 'font-size: 14px;', '@media screen and (max-width: 400px)'); * $concatenator->append(['ul', 'ol'], 'margin: 0.75em 0;', '@media screen and (max-width: 400px)'); * $css = $concatenator->getCss(); * * `$css` (if unminified) would contain the following CSS: * ` body { * ` color: blue; * ` font-size: 16px; * ` } * ` p, ul, ol { * ` margin: 1em 0; * ` } * ` @media screen and (max-width: 400px) { * ` body { * ` font-size: 14px; * ` } * ` ul, ol { * ` margin: 0.75em 0; * ` } * ` } * * @internal */ final class CssConcatenator { /** * Array of media rules in order. Each element is an object with the following properties: * - string `media` - The media query string, e.g. "@media screen and (max-width:639px)", or an empty string for * rules not within a media query block; * - object[] `ruleBlocks` - Array of rule blocks in order, where each element is an object with the following * properties: * - mixed[] `selectorsAsKeys` - Array whose keys are selectors for the rule block (values are of no * significance); * - string `declarationsBlock` - The property declarations, e.g. "margin-top: 0.5em; padding: 0". * * @var array<int, object{ * media: string, * ruleBlocks: array<int, object{ * selectorsAsKeys: array<string, array-key>, * declarationsBlock: string * }> * }> */ private $mediaRules = []; /** * Appends a declaration block to the CSS. * * @param array<array-key, string> $selectors * array of selectors for the rule, e.g. ["ul", "ol", "p:first-child"] * @param string $declarationsBlock * the property declarations, e.g. "margin-top: 0.5em; padding: 0" * @param string $media * the media query for the rule, e.g. "@media screen and (max-width:639px)", or an empty string if none */ public function append(array $selectors, string $declarationsBlock, string $media = ''): void { $selectorsAsKeys = \array_flip($selectors); $mediaRule = $this->getOrCreateMediaRuleToAppendTo($media); $ruleBlocks = $mediaRule->ruleBlocks; $lastRuleBlock = \end($ruleBlocks); $hasSameDeclarationsAsLastRule = \is_object($lastRuleBlock) && $declarationsBlock === $lastRuleBlock->declarationsBlock; if ($hasSameDeclarationsAsLastRule) { $lastRuleBlock->selectorsAsKeys += $selectorsAsKeys; } else { $lastRuleBlockSelectors = \is_object($lastRuleBlock) ? $lastRuleBlock->selectorsAsKeys : []; $hasSameSelectorsAsLastRule = \is_object($lastRuleBlock) && self::hasEquivalentSelectors($selectorsAsKeys, $lastRuleBlockSelectors); if ($hasSameSelectorsAsLastRule) { $lastDeclarationsBlockWithoutSemicolon = \rtrim(\rtrim($lastRuleBlock->declarationsBlock), ';'); $lastRuleBlock->declarationsBlock = $lastDeclarationsBlockWithoutSemicolon . ';' . $declarationsBlock; } else { $mediaRule->ruleBlocks[] = (object) \compact('selectorsAsKeys', 'declarationsBlock'); } } } /** * @return string */ public function getCss(): string { return \implode('', \array_map([self::class, 'getMediaRuleCss'], $this->mediaRules)); } /** * @param string $media The media query for rules to be appended, e.g. "@media screen and (max-width:639px)", * or an empty string if none. * * @return object{ * media: string, * ruleBlocks: array<int, object{ * selectorsAsKeys: array<string, array-key>, * declarationsBlock: string * }> * } */ private function getOrCreateMediaRuleToAppendTo(string $media): object { $lastMediaRule = \end($this->mediaRules); if (\is_object($lastMediaRule) && $media === $lastMediaRule->media) { return $lastMediaRule; } $newMediaRule = (object) [ 'media' => $media, 'ruleBlocks' => [], ]; $this->mediaRules[] = $newMediaRule; return $newMediaRule; } /** * Tests if two sets of selectors are equivalent (i.e. the same selectors, possibly in a different order). * * @param array<string, array-key> $selectorsAsKeys1 * array in which the selectors are the keys, and the values are of no significance * @param array<string, array-key> $selectorsAsKeys2 another such array * * @return bool */ private static function hasEquivalentSelectors(array $selectorsAsKeys1, array $selectorsAsKeys2): bool { return \count($selectorsAsKeys1) === \count($selectorsAsKeys2) && \count($selectorsAsKeys1) === \count($selectorsAsKeys1 + $selectorsAsKeys2); } /** * @param object{ * media: string, * ruleBlocks: array<int, object{ * selectorsAsKeys: array<string, array-key>, * declarationsBlock: string * }> * } $mediaRule * * @return string CSS for the media rule. */ private static function getMediaRuleCss(object $mediaRule): string { $ruleBlocks = $mediaRule->ruleBlocks; $css = \implode('', \array_map([self::class, 'getRuleBlockCss'], $ruleBlocks)); $media = $mediaRule->media; if ($media !== '') { $css = $media . '{' . $css . '}'; } return $css; } /** * @param object{selectorsAsKeys: array<string, array-key>, declarationsBlock: string} $ruleBlock * * @return string CSS for the rule block. */ private static function getRuleBlockCss(object $ruleBlock): string { $selectorsAsKeys = $ruleBlock->selectorsAsKeys; $selectors = \array_keys($selectorsAsKeys); $declarationsBlock = $ruleBlock->declarationsBlock; return \implode(',', $selectors) . '{' . $declarationsBlock . '}'; } } packages/Pelago/Emogrifier/Utilities/DeclarationBlockParser.php 0000644 00000005467 15217646615 0020713 0 ustar 00 <?php declare(strict_types=1); namespace Automattic\WooCommerce\Vendor\Pelago\Emogrifier\Utilities; /** * Provides a common method for parsing CSS declaration blocks. * These might be from actual CSS, or from the `style` attribute of an HTML DOM element. * * Caches results globally. * * @internal */ final class DeclarationBlockParser { /** * @var array<string, array<non-empty-string, string>> */ private static $cache = []; /** * CSS custom properties (variables) have case-sensitive names, so their case must be preserved. * Standard CSS properties have case-insensitive names, which are converted to lowercase. * * @param non-empty-string $name * * @return non-empty-string */ public function normalizePropertyName(string $name): string { if (\substr($name, 0, 2) === '--') { return $name; } else { return \strtolower($name); } } /** * Parses a CSS declaration block into property name/value pairs. * * Example: * * The declaration block * * "color: #000; font-weight: bold;" * * will be parsed into the following array: * * "color" => "#000" * "font-weight" => "bold" * * @param string $declarationBlock the CSS declarations block without the curly braces, may be empty * * @return array<non-empty-string, string> * the CSS declarations with the property names as array keys and the property values as array values * * @throws \UnexpectedValueException if an empty property name is encountered (which cannot happen) */ public function parse(string $declarationBlock): array { if (isset(self::$cache[$declarationBlock])) { return self::$cache[$declarationBlock]; } $preg = new Preg(); $declarations = $preg->split('/;(?!base64|charset)/', $declarationBlock); $properties = []; foreach ($declarations as $declaration) { $matches = []; if ( $preg->match( '/^([A-Za-z\\-]+)\\s*:\\s*(.+)$/s', \trim($declaration), $matches ) === 0 ) { continue; } $propertyName = $matches[1]; if ($propertyName === '') { // This cannot happen since the regular epression matches one or more characters. throw new \UnexpectedValueException('An empty property name was encountered.', 1727046409); } $propertyValue = $matches[2]; $properties[$this->normalizePropertyName($propertyName)] = $propertyValue; } self::$cache[$declarationBlock] = $properties; return $properties; } } packages/Pelago/Emogrifier/Utilities/ArrayIntersector.php 0000644 00000003624 15217646623 0017626 0 ustar 00 <?php declare(strict_types=1); namespace Automattic\WooCommerce\Vendor\Pelago\Emogrifier\Utilities; /** * When computing many array intersections using the same array, it is more efficient to use `array_flip()` first and * then `array_intersect_key()`, than `array_intersect()`. See the discussion at * {@link https://stackoverflow.com/questions/6329211/php-array-intersect-efficiency Stack Overflow} for more * information. * * Of course, this is only possible if the arrays contain integer or string values, and either don't contain duplicates, * or that fact that duplicates will be removed does not matter. * * This class takes care of the detail. * * @internal */ final class ArrayIntersector { /** * the array with which the object was constructed, with all its keys exchanged with their associated values * * @var array<array-key, array-key> */ private $invertedArray; /** * Constructs the object with the array that will be reused for many intersection computations. * * @param array<array-key, array-key> $array */ public function __construct(array $array) { $this->invertedArray = \array_flip($array); } /** * Computes the intersection of `$array` and the array with which this object was constructed. * * @param array<array-key, array-key> $array * * @return array<array-key, array-key> * Returns an array containing all of the values in `$array` whose values exist in the array * with which this object was constructed. Note that keys are preserved, order is maintained, but * duplicates are removed. */ public function intersectWith(array $array): array { $invertedArray = \array_flip($array); $invertedIntersection = \array_intersect_key($invertedArray, $this->invertedArray); return \array_flip($invertedIntersection); } } packages/Pelago/Emogrifier/Css/StyleRule.php 0000644 00000004335 15217646626 0015036 0 ustar 00 <?php declare(strict_types=1); namespace Automattic\WooCommerce\Vendor\Pelago\Emogrifier\Css; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Property\Selector; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\RuleSet\DeclarationBlock; /** * This class represents a CSS style rule, including selectors, a declaration block, and an optional containing at-rule. * * @internal */ final class StyleRule { /** * @var DeclarationBlock */ private $declarationBlock; /** * @var string */ private $containingAtRule; /** * @param DeclarationBlock $declarationBlock * @param string $containingAtRule e.g. `@media screen and (max-width: 480px)` */ public function __construct(DeclarationBlock $declarationBlock, string $containingAtRule = '') { $this->declarationBlock = $declarationBlock; $this->containingAtRule = \trim($containingAtRule); } /** * @return array<int, string> the selectors, e.g. `["h1", "p"]` */ public function getSelectors(): array { /** @var array<int, Selector> $selectors */ $selectors = $this->declarationBlock->getSelectors(); return \array_map( static function (Selector $selector): string { return (string) $selector; }, $selectors ); } /** * @return string the CSS declarations, separated and followed by a semicolon, e.g., `color: red; height: 4px;` */ public function getDeclarationAsText(): string { return \implode(' ', $this->declarationBlock->getRules()); } /** * Checks whether the declaration block has at least one declaration. */ public function hasAtLeastOneDeclaration(): bool { return $this->declarationBlock->getRules() !== []; } /** * @returns string e.g. `@media screen and (max-width: 480px)`, or an empty string */ public function getContainingAtRule(): string { return $this->containingAtRule; } /** * Checks whether the containing at-rule is non-empty and has any non-whitespace characters. */ public function hasContainingAtRule(): bool { return $this->getContainingAtRule() !== ''; } } packages/Pelago/Emogrifier/Css/CssDocument.php 0000644 00000017362 15217646626 0015341 0 ustar 00 <?php declare(strict_types=1); namespace Automattic\WooCommerce\Vendor\Pelago\Emogrifier\Css; use Automattic\WooCommerce\Vendor\Pelago\Emogrifier\Utilities\Preg; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\CSSList\AtRuleBlockList as CssAtRuleBlockList; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\CSSList\Document as SabberwormCssDocument; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Parser as CssParser; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Property\AtRule as CssAtRule; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Property\Charset as CssCharset; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Property\Import as CssImport; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Renderable as CssRenderable; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\RuleSet\DeclarationBlock as CssDeclarationBlock; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\RuleSet\RuleSet as CssRuleSet; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Settings as ParserSettings; /** * Parses and stores a CSS document from a string of CSS, and provides methods to obtain the CSS in parts or as data * structures. * * @internal */ final class CssDocument { /** * @var SabberwormCssDocument */ private $sabberwormCssDocument; /** * `@import` rules must precede all other types of rules, except `@charset` rules. This property is used while * rendering at-rules to enforce that. * * @var bool */ private $isImportRuleAllowed = true; /** * @param string $css * @param bool $debug * If this is `true`, an exception will be thrown if invalid CSS is encountered. * Otherwise the parser will try to do the best it can. */ public function __construct(string $css, bool $debug) { // CSS Parser currently throws exception with nested at-rules (like `@media`) in strict parsing mode $parserSettings = ParserSettings::create()->withLenientParsing(!$debug || $this->hasNestedAtRule($css)); // CSS Parser currently throws exception with non-empty whitespace-only CSS in strict parsing mode, so `trim()` // @see https://github.com/sabberworm/PHP-CSS-Parser/issues/349 $this->sabberwormCssDocument = (new CssParser(\trim($css), $parserSettings))->parse(); } /** * Tests if a string of CSS appears to contain an at-rule with nested rules * (`@media`, `@supports`, `@keyframes`, `@document`, * the latter two additionally with vendor prefixes that may commonly be used). * * @see https://github.com/sabberworm/PHP-CSS-Parser/issues/127 */ private function hasNestedAtRule(string $css): bool { return (new Preg()) ->match('/@(?:media|supports|(?:-webkit-|-moz-|-ms-|-o-)?+(keyframes|document))\\b/', $css) !== 0; } /** * Collates the media query, selectors and declarations for individual rules from the parsed CSS, in order. * * @param array<array-key, string> $allowedMediaTypes * * @return list<StyleRule> */ public function getStyleRulesData(array $allowedMediaTypes): array { $ruleMatches = []; /** @var CssRenderable $rule */ foreach ($this->sabberwormCssDocument->getContents() as $rule) { if ($rule instanceof CssAtRuleBlockList) { $containingAtRule = $this->getFilteredAtIdentifierAndRule($rule, $allowedMediaTypes); if (\is_string($containingAtRule)) { /** @var CssRenderable $nestedRule */ foreach ($rule->getContents() as $nestedRule) { if ($nestedRule instanceof CssDeclarationBlock) { $ruleMatches[] = new StyleRule($nestedRule, $containingAtRule); } } } } elseif ($rule instanceof CssDeclarationBlock) { $ruleMatches[] = new StyleRule($rule); } } return $ruleMatches; } /** * Renders at-rules from the parsed CSS that are valid and not conditional group rules (i.e. not rules such as * `@media` which contain style rules whose data is returned by {@see getStyleRulesData}). Also does not render * `@charset` rules; these are discarded (only UTF-8 is supported). * * @return string */ public function renderNonConditionalAtRules(): string { $this->isImportRuleAllowed = true; $cssContents = $this->sabberwormCssDocument->getContents(); $atRules = \array_filter($cssContents, [$this, 'isValidAtRuleToRender']); if ($atRules === []) { return ''; } $atRulesDocument = new SabberwormCssDocument(); $atRulesDocument->setContents($atRules); return $atRulesDocument->render(); } /** * @param CssAtRuleBlockList $rule * @param array<array-key, string> $allowedMediaTypes * * @return ?string * If the nested at-rule is supported, it's opening declaration (e.g. "@media (max-width: 768px)") is * returned; otherwise the return value is null. */ private function getFilteredAtIdentifierAndRule(CssAtRuleBlockList $rule, array $allowedMediaTypes): ?string { $result = null; if ($rule->atRuleName() === 'media') { $mediaQueryList = $rule->atRuleArgs(); [$mediaType] = \explode('(', $mediaQueryList, 2); if (\trim($mediaType) !== '') { $escapedAllowedMediaTypes = \array_map( static function (string $allowedMediaType): string { return \preg_quote($allowedMediaType, '/'); }, $allowedMediaTypes ); $mediaTypesMatcher = \implode('|', $escapedAllowedMediaTypes); $isAllowed = (new Preg())->match('/^\\s*+(?:only\\s++)?+(?:' . $mediaTypesMatcher . ')/i', $mediaType) !== 0; } else { $isAllowed = true; } if ($isAllowed) { $result = '@media ' . $mediaQueryList; } } return $result; } /** * Tests if a CSS rule is an at-rule that should be passed though and copied to a `<style>` element unmodified: * - `@charset` rules are discarded - only UTF-8 is supported - `false` is returned; * - `@import` rules are passed through only if they satisfy the specification ("user agents must ignore any * '@import' rule that occurs inside a block or after any non-ignored statement other than an '@charset' or an * '@import' rule"); * - `@media` rules are processed separately to see if their nested rules apply - `false` is returned; * - `@font-face` rules are checked for validity - they must contain both a `src` and `font-family` property; * - other at-rules are assumed to be valid and treated as a black box - `true` is returned. * * @param CssRenderable $rule * * @return bool */ private function isValidAtRuleToRender(CssRenderable $rule): bool { if ($rule instanceof CssCharset) { return false; } if ($rule instanceof CssImport) { return $this->isImportRuleAllowed; } $this->isImportRuleAllowed = false; if (!$rule instanceof CssAtRule) { return false; } switch ($rule->atRuleName()) { case 'media': $result = false; break; case 'font-face': $result = $rule instanceof CssRuleSet && $rule->getRules('font-family') !== [] && $rule->getRules('src') !== []; break; default: $result = true; } return $result; } } packages/Pelago/Emogrifier/Caching/SimpleStringCache.php 0000644 00000004075 15217646626 0017257 0 ustar 00 <?php declare(strict_types=1); namespace Automattic\WooCommerce\Vendor\Pelago\Emogrifier\Caching; /** * This cache caches string values with string keys. It is not PSR-6-compliant. * * Usage: * * ```php * $cache = new SimpleStringCache(); * $cache->set($key, $value); * … * if ($cache->has($key) { * $cachedValue = $cache->get($value); * } * ``` * * @internal */ final class SimpleStringCache { /** * @var array<string, string> */ private $values = []; /** * Checks whether there is an entry stored for the given key. * * @param string $key the key to check; must not be empty * * @throws \InvalidArgumentException */ public function has(string $key): bool { $this->assertNotEmptyKey($key); return isset($this->values[$key]); } /** * Returns the entry stored for the given key, and throws an exception if the value does not exist * (which helps keep the return type simple). * * @param string $key the key to of the item to retrieve; must not be empty * * @return string the retrieved value; may be empty * * @throws \BadMethodCallException */ public function get(string $key): string { if (!$this->has($key)) { throw new \BadMethodCallException('You can only call `get` with a key for an existing value.', 1625996246); } return $this->values[$key]; } /** * Sets or overwrites an entry. * * @param string $key the key to of the item to set; must not be empty * @param string $value the value to set; can be empty * * @throws \BadMethodCallException */ public function set(string $key, string $value): void { $this->assertNotEmptyKey($key); $this->values[$key] = $value; } /** * @throws \InvalidArgumentException */ private function assertNotEmptyKey(string $key): void { if ($key === '') { throw new \InvalidArgumentException('Please provide a non-empty key.', 1625995840); } } } packages/Pelago/Emogrifier/HtmlProcessor/CssVariableEvaluator.php 0000644 00000016263 15217646626 0021246 0 ustar 00 <?php declare(strict_types=1); namespace Automattic\WooCommerce\Vendor\Pelago\Emogrifier\HtmlProcessor; use Automattic\WooCommerce\Vendor\Pelago\Emogrifier\Utilities\DeclarationBlockParser; use Automattic\WooCommerce\Vendor\Pelago\Emogrifier\Utilities\Preg; /** * This class can evaluate CSS custom properties that are defined and used in inline style attributes. */ final class CssVariableEvaluator extends AbstractHtmlProcessor { /** * temporary collection used by {@see replaceVariablesInDeclarations} and callee methods * * @var array<non-empty-string, string> */ private $currentVariableDefinitions = []; /** * Replaces all CSS custom property references in inline style attributes with their corresponding values where * defined in inline style attributes (either from the element itself or the nearest ancestor). * * @return $this * * @throws \UnexpectedValueException */ public function evaluateVariables(): self { return $this->evaluateVariablesInElementAndDescendants($this->getHtmlElement(), []); } /** * @param array<non-empty-string, string> $declarations * * @return array<non-empty-string, string> */ private function getVariableDefinitionsFromDeclarations(array $declarations): array { return \array_filter( $declarations, static function (string $key): bool { return \substr($key, 0, 2) === '--'; }, ARRAY_FILTER_USE_KEY ); } /** * Callback function for {@see replaceVariablesInPropertyValue} performing regular expression replacement. * * @param array<int, string> $matches */ private function getPropertyValueReplacement(array $matches): string { $variableName = $matches[1]; if (isset($this->currentVariableDefinitions[$variableName])) { $variableValue = $this->currentVariableDefinitions[$variableName]; } else { $fallbackValueSeparator = $matches[2] ?? ''; if ($fallbackValueSeparator !== '') { $fallbackValue = $matches[3]; // The fallback value may use other CSS variables, so recurse $variableValue = $this->replaceVariablesInPropertyValue($fallbackValue); } else { $variableValue = $matches[0]; } } return $variableValue; } /** * Regular expression based on {@see https://stackoverflow.com/a/54143883/2511031 a StackOverflow answer}. */ private function replaceVariablesInPropertyValue(string $propertyValue): string { return (new Preg())->replaceCallback( '/ var\\( \\s*+ # capture variable name including `--` prefix ( --[^\\s\\),]++ ) \\s*+ # capture optional fallback value (?: # capture separator to confirm there is a fallback value (,)\\s* # begin capture with named group that can be used recursively (?<recursable> # begin named group to match sequence without parentheses, except in strings (?<noparentheses> # repeated zero or more times: (?: # sequence without parentheses or quotes [^\\(\\)\'"]++ | # string in double quotes "(?>[^"\\\\]++|\\\\.)*" | # string in single quotes \'(?>[^\'\\\\]++|\\\\.)*\' )*+ ) # repeated zero or more times: (?: # sequence in parentheses \\( # using the named recursable pattern (?&recursable) \\) # sequence without parentheses, except in strings (?&noparentheses) )*+ ) )?+ \\) /x', \Closure::fromCallable([$this, 'getPropertyValueReplacement']), $propertyValue ); } /** * @param array<non-empty-string, string> $declarations * * @return ?array<non-empty-string, string> `null` is returned if no substitutions were made. */ private function replaceVariablesInDeclarations(array $declarations): ?array { $substitutionsMade = false; $result = \array_map( function (string $propertyValue) use (&$substitutionsMade): string { $newPropertyValue = $this->replaceVariablesInPropertyValue($propertyValue); if ($newPropertyValue !== $propertyValue) { $substitutionsMade = true; } return $newPropertyValue; }, $declarations ); return $substitutionsMade ? $result : null; } /** * @param array<non-empty-string, string> $declarations */ private function getDeclarationsAsString(array $declarations): string { $declarationStrings = \array_map( static function (string $key, string $value): string { return $key . ': ' . $value; }, \array_keys($declarations), \array_values($declarations) ); return \implode('; ', $declarationStrings) . ';'; } /** * @param array<non-empty-string, string> $ancestorVariableDefinitions * * @return $this */ private function evaluateVariablesInElementAndDescendants( \DOMElement $element, array $ancestorVariableDefinitions ): self { $style = $element->getAttribute('style'); // Avoid parsing declarations if none use or define a variable if ((new Preg())->match('/(?<![\\w\\-])--[\\w\\-]/', $style) !== 0) { $declarations = (new DeclarationBlockParser())->parse($style); $variableDefinitions = $this->currentVariableDefinitions = $this->getVariableDefinitionsFromDeclarations($declarations) + $ancestorVariableDefinitions; $newDeclarations = $this->replaceVariablesInDeclarations($declarations); if ($newDeclarations !== null) { $element->setAttribute('style', $this->getDeclarationsAsString($newDeclarations)); } } else { $variableDefinitions = $ancestorVariableDefinitions; } foreach ($element->childNodes as $child) { if ($child instanceof \DOMElement) { $this->evaluateVariablesInElementAndDescendants($child, $variableDefinitions); } } return $this; } } packages/Pelago/Emogrifier/HtmlProcessor/HtmlPruner.php 0000644 00000012266 15217646626 0017264 0 ustar 00 <?php declare(strict_types=1); namespace Automattic\WooCommerce\Vendor\Pelago\Emogrifier\HtmlProcessor; use Automattic\WooCommerce\Vendor\Pelago\Emogrifier\CssInliner; use Automattic\WooCommerce\Vendor\Pelago\Emogrifier\Utilities\ArrayIntersector; use Automattic\WooCommerce\Vendor\Pelago\Emogrifier\Utilities\Preg; /** * This class can remove things from HTML. */ final class HtmlPruner extends AbstractHtmlProcessor { /** * We need to look for display:none, but we need to do a case-insensitive search. Since DOMDocument only * supports XPath 1.0, lower-case() isn't available to us. We've thus far only set attributes to lowercase, * not attribute values. Consequently, we need to translate() the letters that would be in 'NONE' ("NOE") * to lowercase. * * @var string */ private const DISPLAY_NONE_MATCHER = '//*[@style and contains(translate(translate(@style," ",""),"NOE","noe"),"display:none")' . ' and not(@class and contains(concat(" ", normalize-space(@class), " "), " -emogrifier-keep "))]'; /** * Removes elements that have a "display: none;" style. * * @return $this */ public function removeElementsWithDisplayNone(): self { $elementsWithStyleDisplayNone = $this->getXPath()->query(self::DISPLAY_NONE_MATCHER); if ($elementsWithStyleDisplayNone->length === 0) { return $this; } foreach ($elementsWithStyleDisplayNone as $element) { $parentNode = $element->parentNode; if ($parentNode !== null) { $parentNode->removeChild($element); } } return $this; } /** * Removes classes that are no longer required (e.g. because there are no longer any CSS rules that reference them) * from `class` attributes. * * Note that this does not inspect the CSS, but expects to be provided with a list of classes that are still in use. * * This method also has the (presumably beneficial) side-effect of minifying (removing superfluous whitespace from) * `class` attributes. * * @param array<array-key, string> $classesToKeep names of classes that should not be removed * * @return $this */ public function removeRedundantClasses(array $classesToKeep = []): self { $elementsWithClassAttribute = $this->getXPath()->query('//*[@class]'); if ($classesToKeep !== []) { $this->removeClassesFromElements($elementsWithClassAttribute, $classesToKeep); } else { // Avoid unnecessary processing if there are no classes to keep. $this->removeClassAttributeFromElements($elementsWithClassAttribute); } return $this; } /** * Removes classes from the `class` attribute of each element in `$elements`, except any in `$classesToKeep`, * removing the `class` attribute itself if the resultant list is empty. * * @param \DOMNodeList $elements * @param array<array-key, string> $classesToKeep */ private function removeClassesFromElements(\DOMNodeList $elements, array $classesToKeep): void { $classesToKeepIntersector = new ArrayIntersector($classesToKeep); $preg = new Preg(); /** @var \DOMElement $element */ foreach ($elements as $element) { $elementClasses = $preg->split('/\\s++/', \trim($element->getAttribute('class'))); $elementClassesToKeep = $classesToKeepIntersector->intersectWith($elementClasses); if ($elementClassesToKeep !== []) { $element->setAttribute('class', \implode(' ', $elementClassesToKeep)); } else { $element->removeAttribute('class'); } } } /** * Removes the `class` attribute from each element in `$elements`. * * @param \DOMNodeList $elements */ private function removeClassAttributeFromElements(\DOMNodeList $elements): void { /** @var \DOMElement $element */ foreach ($elements as $element) { $element->removeAttribute('class'); } } /** * After CSS has been inlined, there will likely be some classes in `class` attributes that are no longer referenced * by any remaining (uninlinable) CSS. This method removes such classes. * * Note that it does not inspect the remaining CSS, but uses information readily available from the `CssInliner` * instance about the CSS rules that could not be inlined. * * @param CssInliner $cssInliner object instance that performed the CSS inlining * * @return $this * * @throws \BadMethodCallException if `inlineCss` has not first been called on `$cssInliner` */ public function removeRedundantClassesAfterCssInlined(CssInliner $cssInliner): self { $preg = new Preg(); $classesToKeepAsKeys = []; foreach ($cssInliner->getMatchingUninlinableSelectors() as $selector) { $preg->matchAll('/\\.(-?+[_a-zA-Z][\\w\\-]*+)/', $selector, $matches); $classesToKeepAsKeys += \array_fill_keys($matches[1], true); } $this->removeRedundantClasses(\array_keys($classesToKeepAsKeys)); return $this; } } packages/Pelago/Emogrifier/HtmlProcessor/AbstractHtmlProcessor.php 0000644 00000035711 15217646626 0021454 0 ustar 00 <?php declare(strict_types=1); namespace Automattic\WooCommerce\Vendor\Pelago\Emogrifier\HtmlProcessor; use Automattic\WooCommerce\Vendor\Pelago\Emogrifier\Utilities\Preg; /** * Base class for HTML processor that e.g., can remove, add or modify nodes or attributes. * * The "vanilla" subclass is the HtmlNormalizer. */ abstract class AbstractHtmlProcessor { /** * @var string */ protected const DEFAULT_DOCUMENT_TYPE = '<!DOCTYPE html>'; /** * @var string */ protected const CONTENT_TYPE_META_TAG = '<meta http-equiv="Content-Type" content="text/html; charset=utf-8">'; /** * @var string Regular expression part to match tag names that PHP's DOMDocument implementation is not aware are * self-closing. These are mostly HTML5 elements, but for completeness <command> (obsolete) and <keygen> * (deprecated) are also included. * * @see https://bugs.php.net/bug.php?id=73175 */ protected const PHP_UNRECOGNIZED_VOID_TAGNAME_MATCHER = '(?:command|embed|keygen|source|track|wbr)'; /** * Regular expression part to match tag names that may appear before the start of the `<body>` element. A start tag * for any other element would implicitly start the `<body>` element due to tag omission rules. * * @var string */ protected const TAGNAME_ALLOWED_BEFORE_BODY_MATCHER = '(?:html|head|base|command|link|meta|noscript|script|style|template|title)'; /** * regular expression pattern to match an HTML comment, including delimiters and modifiers * * @var string */ protected const HTML_COMMENT_PATTERN = '/<!--[^-]*+(?:-(?!->)[^-]*+)*+(?:-->|$)/'; /** * regular expression pattern to match an HTML `<template>` element, including delimiters and modifiers * * @var string */ protected const HTML_TEMPLATE_ELEMENT_PATTERN = '%<template[\\s>][^<]*+(?:<(?!/template>)[^<]*+)*+(?:</template>|$)%i'; /** * @var ?\DOMDocument */ protected $domDocument = null; /** * @var ?\DOMXPath */ private $xPath = null; /** * The constructor. * * Please use `::fromHtml` or `::fromDomDocument` instead. */ private function __construct() {} /** * Builds a new instance from the given HTML. * * @param string $unprocessedHtml raw HTML, must be UTF-encoded, must not be empty * * @return static * * @throws \InvalidArgumentException if $unprocessedHtml is anything other than a non-empty string */ public static function fromHtml(string $unprocessedHtml): self { if ($unprocessedHtml === '') { throw new \InvalidArgumentException('The provided HTML must not be empty.', 1515763647); } $instance = new static(); $instance->setHtml($unprocessedHtml); return $instance; } /** * Builds a new instance from the given DOM document. * * @param \DOMDocument $document a DOM document returned by getDomDocument() of another instance * * @return static */ public static function fromDomDocument(\DOMDocument $document): self { $instance = new static(); $instance->setDomDocument($document); return $instance; } /** * Sets the HTML to process. * * @param string $html the HTML to process, must be UTF-8-encoded */ private function setHtml(string $html): void { $this->createUnifiedDomDocument($html); } /** * Provides access to the internal DOMDocument representation of the HTML in its current state. * * @return \DOMDocument * * @throws \UnexpectedValueException */ public function getDomDocument(): \DOMDocument { if (!$this->domDocument instanceof \DOMDocument) { $message = self::class . '::setDomDocument() has not yet been called on ' . static::class; throw new \UnexpectedValueException($message, 1570472239); } return $this->domDocument; } /** * @param \DOMDocument $domDocument */ private function setDomDocument(\DOMDocument $domDocument): void { $this->domDocument = $domDocument; $this->xPath = new \DOMXPath($this->domDocument); } /** * @return \DOMXPath * * @throws \UnexpectedValueException */ protected function getXPath(): \DOMXPath { if (!$this->xPath instanceof \DOMXPath) { $message = self::class . '::setDomDocument() has not yet been called on ' . static::class; throw new \UnexpectedValueException($message, 1617819086); } return $this->xPath; } /** * Renders the normalized and processed HTML. * * @return string */ public function render(): string { $htmlWithPossibleErroneousClosingTags = $this->getDomDocument()->saveHTML(); return $this->removeSelfClosingTagsClosingTags($htmlWithPossibleErroneousClosingTags); } /** * Renders the content of the BODY element of the normalized and processed HTML. * * @return string */ public function renderBodyContent(): string { $htmlWithPossibleErroneousClosingTags = $this->getDomDocument()->saveHTML($this->getBodyElement()); $bodyNodeHtml = $this->removeSelfClosingTagsClosingTags($htmlWithPossibleErroneousClosingTags); return (new Preg())->replace('%</?+body(?:\\s[^>]*+)?+>%', '', $bodyNodeHtml); } /** * Eliminates any invalid closing tags for void elements from the given HTML. * * @param string $html * * @return string */ private function removeSelfClosingTagsClosingTags(string $html): string { return (new Preg())->replace('%</' . self::PHP_UNRECOGNIZED_VOID_TAGNAME_MATCHER . '>%', '', $html); } /** * Returns the HTML element. * * This method assumes that there always is an HTML element, throwing an exception otherwise. * * @throws \UnexpectedValueException */ protected function getHtmlElement(): \DOMElement { $htmlElement = $this->getDomDocument()->getElementsByTagName('html')->item(0); if (!$htmlElement instanceof \DOMElement) { throw new \UnexpectedValueException('There is no HTML element although there should be one.', 1569930853); } return $htmlElement; } /** * Returns the BODY element. * * This method assumes that there always is a BODY element. * * @return \DOMElement * * @throws \RuntimeException */ private function getBodyElement(): \DOMElement { $node = $this->getDomDocument()->getElementsByTagName('body')->item(0); if (!$node instanceof \DOMElement) { throw new \RuntimeException('There is no body element.', 1617922607); } return $node; } /** * Creates a DOM document from the given HTML and stores it in $this->domDocument. * * The DOM document will always have a BODY element and a document type. * * @param string $html */ private function createUnifiedDomDocument(string $html): void { $this->createRawDomDocument($html); $this->ensureExistenceOfBodyElement(); } /** * Creates a DOMDocument instance from the given HTML and stores it in $this->domDocument. * * @param string $html */ private function createRawDomDocument(string $html): void { $domDocument = new \DOMDocument(); $domDocument->strictErrorChecking = false; $domDocument->formatOutput = false; $libXmlState = \libxml_use_internal_errors(true); $domDocument->loadHTML($this->prepareHtmlForDomConversion($html)); \libxml_clear_errors(); \libxml_use_internal_errors($libXmlState); $this->setDomDocument($domDocument); } /** * Returns the HTML with added document type, Content-Type meta tag, and self-closing slashes, if needed, * ensuring that the HTML will be good for creating a DOM document from it. * * @param string $html * * @return string the unified HTML */ private function prepareHtmlForDomConversion(string $html): string { $htmlWithSelfClosingSlashes = $this->ensurePhpUnrecognizedSelfClosingTagsAreXml($html); $htmlWithDocumentType = $this->ensureDocumentType($htmlWithSelfClosingSlashes); return $this->addContentTypeMetaTag($htmlWithDocumentType); } /** * Makes sure that the passed HTML has a document type, with lowercase "html". * * @param string $html * * @return string HTML with document type */ private function ensureDocumentType(string $html): string { $hasDocumentType = \stripos($html, '<!DOCTYPE') !== false; if ($hasDocumentType) { return $this->normalizeDocumentType($html); } return self::DEFAULT_DOCUMENT_TYPE . $html; } /** * Makes sure the document type in the passed HTML has lowercase "html". * * @param string $html * * @return string HTML with normalized document type */ private function normalizeDocumentType(string $html): string { // Limit to replacing the first occurrence: as an optimization; and in case an example exists as unescaped text. return (new Preg())->replace( '/<!DOCTYPE\\s++html(?=[\\s>])/i', '<!DOCTYPE html', $html, 1 ); } /** * Adds a Content-Type meta tag for the charset. * * This method also ensures that there is a HEAD element. * * @param string $html * * @return string the HTML with the meta tag added */ private function addContentTypeMetaTag(string $html): string { if ($this->hasContentTypeMetaTagInHead($html)) { return $html; } // We are trying to insert the meta tag to the right spot in the DOM. // If we just prepended it to the HTML, we would lose attributes set to the HTML tag. $hasHeadTag = (new Preg())->match('/<head[\\s>]/i', $html) !== 0; $hasHtmlTag = \stripos($html, '<html') !== false; if ($hasHeadTag) { $reworkedHtml = (new Preg())->replace( '/<head(?=[\\s>])([^>]*+)>/i', '<head$1>' . self::CONTENT_TYPE_META_TAG, $html ); } elseif ($hasHtmlTag) { $reworkedHtml = (new Preg())->replace( '/<html(.*?)>/is', '<html$1><head>' . self::CONTENT_TYPE_META_TAG . '</head>', $html ); } else { $reworkedHtml = self::CONTENT_TYPE_META_TAG . $html; } return $reworkedHtml; } /** * Tests whether the given HTML has a valid `Content-Type` metadata element within the `<head>` element. Due to tag * omission rules, HTML parsers are expected to end the `<head>` element and start the `<body>` element upon * encountering a start tag for any element which is permitted only within the `<body>`. * * @param string $html * * @return bool */ private function hasContentTypeMetaTagInHead(string $html): bool { (new Preg())->match( '%^.*?(?=<meta(?=\\s)[^>]*\\shttp-equiv=(["\']?+)Content-Type\\g{-1}[\\s/>])%is', $html, $matches ); if (isset($matches[0])) { $htmlBefore = $matches[0]; try { $hasContentTypeMetaTagInHead = !$this->hasEndOfHeadElement($htmlBefore); } catch (\RuntimeException $exception) { // If something unexpected occurs, assume the `Content-Type` that was found is valid. \trigger_error($exception->getMessage()); $hasContentTypeMetaTagInHead = true; } } else { $hasContentTypeMetaTagInHead = false; } return $hasContentTypeMetaTagInHead; } /** * Tests whether the `<head>` element ends within the given HTML. Due to tag omission rules, HTML parsers are * expected to end the `<head>` element and start the `<body>` element upon encountering a start tag for any element * which is permitted only within the `<body>`. * * @param string $html * * @return bool * * @throws \RuntimeException */ private function hasEndOfHeadElement(string $html): bool { if ( (new Preg())->match('%<(?!' . self::TAGNAME_ALLOWED_BEFORE_BODY_MATCHER . '[\\s/>])\\w|</head>%i', $html) !== 0 ) { // An exception to the implicit end of the `<head>` is any content within a `<template>` element, as well in // comments. As an optimization, this is only checked for if a potential `<head>` end tag is found. $htmlWithoutCommentsOrTemplates = $this->removeHtmlTemplateElements($this->removeHtmlComments($html)); $hasEndOfHeadElement = $htmlWithoutCommentsOrTemplates === $html || $this->hasEndOfHeadElement($htmlWithoutCommentsOrTemplates); } else { $hasEndOfHeadElement = false; } return $hasEndOfHeadElement; } /** * Removes comments from the given HTML, including any which are unterminated, for which the remainder of the string * is removed. * * @param string $html * * @return string * * @throws \RuntimeException */ private function removeHtmlComments(string $html): string { return (new Preg())->throwExceptions(true)->replace(self::HTML_COMMENT_PATTERN, '', $html); } /** * Removes `<template>` elements from the given HTML, including any without an end tag, for which the remainder of * the string is removed. * * @param string $html * * @return string * * @throws \RuntimeException */ private function removeHtmlTemplateElements(string $html): string { return (new Preg())->throwExceptions(true)->replace(self::HTML_TEMPLATE_ELEMENT_PATTERN, '', $html); } /** * Makes sure that any self-closing tags not recognized as such by PHP's DOMDocument implementation have a * self-closing slash. * * @param string $html * * @return string HTML with problematic tags converted. */ private function ensurePhpUnrecognizedSelfClosingTagsAreXml(string $html): string { return (new Preg())->replace( '%<' . self::PHP_UNRECOGNIZED_VOID_TAGNAME_MATCHER . '\\b[^>]*+(?<!/)(?=>)%', '$0/', $html ); } /** * Checks that $this->domDocument has a BODY element and adds it if it is missing. * * @throws \UnexpectedValueException */ private function ensureExistenceOfBodyElement(): void { if ($this->getDomDocument()->getElementsByTagName('body')->item(0) instanceof \DOMElement) { return; } $this->getHtmlElement()->appendChild($this->getDomDocument()->createElement('body')); } } packages/Pelago/Emogrifier/HtmlProcessor/CssToAttributeConverter.php 0000644 00000021451 15217646626 0021767 0 ustar 00 <?php declare(strict_types=1); namespace Automattic\WooCommerce\Vendor\Pelago\Emogrifier\HtmlProcessor; use Automattic\WooCommerce\Vendor\Pelago\Emogrifier\Utilities\DeclarationBlockParser; use Automattic\WooCommerce\Vendor\Pelago\Emogrifier\Utilities\Preg; /** * This HtmlProcessor can convert style HTML attributes to the corresponding other visual HTML attributes, * e.g. it converts style="width: 100px" to width="100". * * It will only add attributes, but leaves the style attribute untouched. * * To trigger the conversion, call the convertCssToVisualAttributes method. */ final class CssToAttributeConverter extends AbstractHtmlProcessor { /** * This multi-level array contains simple mappings of CSS properties to * HTML attributes. If a mapping only applies to certain HTML nodes or * only for certain values, the mapping is an object with an allowlist * of nodes and values. * * @var array<string, array{attribute: string, nodes?: array<int, string>, values?: array<int, string>}> */ private $cssToHtmlMap = [ 'background-color' => [ 'attribute' => 'bgcolor', ], 'text-align' => [ 'attribute' => 'align', 'nodes' => ['p', 'div', 'td', 'th'], 'values' => ['left', 'right', 'center', 'justify'], ], 'float' => [ 'attribute' => 'align', 'nodes' => ['table', 'img'], 'values' => ['left', 'right'], ], 'border-spacing' => [ 'attribute' => 'cellspacing', 'nodes' => ['table'], ], ]; /** * Maps the CSS from the style nodes to visual HTML attributes. * * @return $this */ public function convertCssToVisualAttributes(): self { $declarationBlockParser = new DeclarationBlockParser(); /** @var \DOMElement $node */ foreach ($this->getAllNodesWithStyleAttribute() as $node) { $inlineStyleDeclarations = $declarationBlockParser->parse($node->getAttribute('style')); $this->mapCssToHtmlAttributes($inlineStyleDeclarations, $node); } return $this; } /** * Returns a list with all DOM nodes that have a style attribute. * * @return \DOMNodeList */ private function getAllNodesWithStyleAttribute(): \DOMNodeList { return $this->getXPath()->query('//*[@style]'); } /** * Applies $styles to $node. * * This method maps CSS styles to HTML attributes and adds those to the * node. * * @param array<string, string> $styles the new CSS styles taken from the global styles to be applied to this node * @param \DOMElement $node node to apply styles to */ private function mapCssToHtmlAttributes(array $styles, \DOMElement $node): void { foreach ($styles as $property => $value) { // Strip !important indicator $value = \trim(\str_replace('!important', '', $value)); $this->mapCssToHtmlAttribute($property, $value, $node); } } /** * Tries to apply the CSS style to $node as an attribute. * * This method maps a CSS rule to HTML attributes and adds those to the node. * * @param string $property the name of the CSS property to map * @param string $value the value of the style rule to map * @param \DOMElement $node node to apply styles to */ private function mapCssToHtmlAttribute(string $property, string $value, \DOMElement $node): void { if (!$this->mapSimpleCssProperty($property, $value, $node)) { $this->mapComplexCssProperty($property, $value, $node); } } /** * Looks up the CSS property in the mapping table and maps it if it matches the conditions. * * @param string $property the name of the CSS property to map * @param string $value the value of the style rule to map * @param \DOMElement $node node to apply styles to * * @return bool true if the property can be mapped using the simple mapping table */ private function mapSimpleCssProperty(string $property, string $value, \DOMElement $node): bool { if (!isset($this->cssToHtmlMap[$property])) { return false; } $mapping = $this->cssToHtmlMap[$property]; $nodesMatch = !isset($mapping['nodes']) || \in_array($node->nodeName, $mapping['nodes'], true); $valuesMatch = !isset($mapping['values']) || \in_array($value, $mapping['values'], true); $canBeMapped = $nodesMatch && $valuesMatch; if ($canBeMapped) { $node->setAttribute($mapping['attribute'], $value); } return $canBeMapped; } /** * Maps CSS properties that need special transformation to an HTML attribute. * * @param string $property the name of the CSS property to map * @param string $value the value of the style rule to map * @param \DOMElement $node node to apply styles to */ private function mapComplexCssProperty(string $property, string $value, \DOMElement $node): void { switch ($property) { case 'background': $this->mapBackgroundProperty($node, $value); break; case 'width': // intentional fall-through case 'height': $this->mapWidthOrHeightProperty($node, $value, $property); break; case 'margin': $this->mapMarginProperty($node, $value); break; case 'border': $this->mapBorderProperty($node, $value); break; default: } } /** * @param \DOMElement $node node to apply styles to * @param string $value the value of the style rule to map */ private function mapBackgroundProperty(\DOMElement $node, string $value): void { // parse out the color, if any /** @var array<int, string> $styles */ $styles = \explode(' ', $value, 2); $first = $styles[0]; if (\is_numeric($first[0]) || \strncmp($first, 'url', 3) === 0) { return; } // as this is not a position or image, assume it's a color $node->setAttribute('bgcolor', $first); } /** * @param \DOMElement $node node to apply styles to * @param string $value the value of the style rule to map * @param string $property the name of the CSS property to map */ private function mapWidthOrHeightProperty(\DOMElement $node, string $value, string $property): void { $preg = new Preg(); // only parse values in px and %, but not values like "auto" if ($preg->match('/^(\\d+)(\\.(\\d+))?(px|%)$/', $value) === 0) { return; } $number = $preg->replace('/[^0-9.%]/', '', $value); $node->setAttribute($property, $number); } /** * @param \DOMElement $node node to apply styles to * @param string $value the value of the style rule to map */ private function mapMarginProperty(\DOMElement $node, string $value): void { if (!$this->isTableOrImageNode($node)) { return; } $margins = $this->parseCssShorthandValue($value); if ($margins['left'] === 'auto' && $margins['right'] === 'auto') { $node->setAttribute('align', 'center'); } } /** * @param \DOMElement $node node to apply styles to * @param string $value the value of the style rule to map */ private function mapBorderProperty(\DOMElement $node, string $value): void { if (!$this->isTableOrImageNode($node)) { return; } if ($value === 'none' || $value === '0') { $node->setAttribute('border', '0'); } } /** * @param \DOMElement $node * * @return bool */ private function isTableOrImageNode(\DOMElement $node): bool { return $node->nodeName === 'table' || $node->nodeName === 'img'; } /** * Parses a shorthand CSS value and splits it into individual values. For example: `padding: 0 auto;` - `0 auto` is * split into top: 0, left: auto, bottom: 0, right: auto. * * @param string $value a CSS property value with 1, 2, 3 or 4 sizes * * @return array<string, string> * an array of values for top, right, bottom and left (using these as associative array keys) */ private function parseCssShorthandValue(string $value): array { $values = (new Preg())->split('/\\s+/', $value); $css = []; $css['top'] = $values[0]; $css['right'] = (\count($values) > 1) ? $values[1] : $css['top']; $css['bottom'] = (\count($values) > 2) ? $values[2] : $css['top']; $css['left'] = (\count($values) > 3) ? $values[3] : $css['right']; return $css; } } packages/Pelago/Emogrifier/HtmlProcessor/HtmlNormalizer.php 0000644 00000000545 15217646626 0020130 0 ustar 00 <?php declare(strict_types=1); namespace Automattic\WooCommerce\Vendor\Pelago\Emogrifier\HtmlProcessor; /** * Normalizes HTML: * - add a document type (HTML5) if missing * - disentangle incorrectly nested tags * - add HEAD and BODY elements (if they are missing) * - reformat the HTML */ final class HtmlNormalizer extends AbstractHtmlProcessor {} packages/Pelago/Emogrifier/CssInliner.php 0000644 00000126735 15217646626 0014440 0 ustar 00 <?php declare(strict_types=1); namespace Automattic\WooCommerce\Vendor\Pelago\Emogrifier; use Automattic\WooCommerce\Vendor\Pelago\Emogrifier\Css\CssDocument; use Automattic\WooCommerce\Vendor\Pelago\Emogrifier\HtmlProcessor\AbstractHtmlProcessor; use Automattic\WooCommerce\Vendor\Pelago\Emogrifier\Utilities\CssConcatenator; use Automattic\WooCommerce\Vendor\Pelago\Emogrifier\Utilities\DeclarationBlockParser; use Automattic\WooCommerce\Vendor\Pelago\Emogrifier\Utilities\Preg; use Automattic\WooCommerce\Vendor\Symfony\Component\CssSelector\CssSelectorConverter; use Automattic\WooCommerce\Vendor\Symfony\Component\CssSelector\Exception\ParseException; /** * This class provides functions for converting CSS styles into inline style attributes in your HTML code. */ final class CssInliner extends AbstractHtmlProcessor { /** * @var int */ private const CACHE_KEY_SELECTOR = 0; /** * @var int */ private const CACHE_KEY_COMBINED_STYLES = 1; /** * Regular expression component matching a static pseudo class in a selector, without the preceding ":", * for which the applicable elements can be determined (by converting the selector to an XPath expression). * (Contains alternation without a group and is intended to be placed within a capturing, non-capturing or lookahead * group, as appropriate for the usage context.) * * @var string */ private const PSEUDO_CLASS_MATCHER = 'empty|(?:first|last|nth(?:-last)?+|only)-(?:child|of-type)|not\\([[:ascii:]]*\\)|root'; /** * This regular expression componenet matches an `...of-type` pseudo class name, without the preceding ":". These * pseudo-classes can currently online be inlined if they have an associated type in the selector expression. * * @var string */ private const OF_TYPE_PSEUDO_CLASS_MATCHER = '(?:first|last|nth(?:-last)?+|only)-of-type'; /** * regular expression component to match a selector combinator * * @var string */ private const COMBINATOR_MATCHER = '(?:\\s++|\\s*+[>+~]\\s*+)(?=[[:alpha:]_\\-.#*:\\[])'; /** * options array key for `querySelectorAll` * * @var string */ private const QSA_ALWAYS_THROW_PARSE_EXCEPTION = 'alwaysThrowParseException'; /** * @var array<string, bool> */ private $excludedSelectors = []; /** * @var array<non-empty-string, bool> */ private $excludedCssSelectors = []; /** * @var array<string, bool> */ private $allowedMediaTypes = ['all' => true, 'screen' => true, 'print' => true]; /** * @var array{ * 0: array<string, int>, * 1: array<string, string> * } */ private $caches = [ self::CACHE_KEY_SELECTOR => [], self::CACHE_KEY_COMBINED_STYLES => [], ]; /** * @var ?CssSelectorConverter */ private $cssSelectorConverter = null; /** * the visited nodes with the XPath paths as array keys * * @var array<string, \DOMElement> */ private $visitedNodes = []; /** * the styles to apply to the nodes with the XPath paths as array keys for the outer array * and the attribute names/values as key/value pairs for the inner array * * @var array<string, array<string, string>> */ private $styleAttributesForNodes = []; /** * Determines whether the "style" attributes of tags in the the HTML passed to this class should be preserved. * If set to false, the value of the style attributes will be discarded. * * @var bool */ private $isInlineStyleAttributesParsingEnabled = true; /** * Determines whether the `<style>` blocks in the HTML passed to this class should be parsed. * * If set to true, the `<style>` blocks will be removed from the HTML and their contents will be applied to the HTML * via inline styles. * * If set to false, the `<style>` blocks will be left as they are in the HTML. * * @var bool */ private $isStyleBlocksParsingEnabled = true; /** * For calculating selector precedence order. * Keys are a regular expression part to match before a CSS name. * Values are a multiplier factor per match to weight specificity. * * @var array<string, int> */ private $selectorPrecedenceMatchers = [ // IDs: worth 10000 '\\#' => 10000, // classes, attributes, pseudo-classes (not pseudo-elements) except `:not`: worth 100 '(?:\\.|\\[|(?<!:):(?!not\\())' => 100, // elements (not attribute values or `:not`), pseudo-elements: worth 1 '(?:(?<![="\':\\w\\-])|::)' => 1, ]; /** * array of data describing CSS rules which apply to the document but cannot be inlined, in the format returned by * {@see collateCssRules} * * @var array<array-key, array{ * media: string, * selector: string, * hasUnmatchablePseudo: bool, * declarationsBlock: string, * line: int * }>|null */ private $matchingUninlinableCssRules = null; /** * Emogrifier will throw Exceptions when it encounters an error instead of silently ignoring them. * * @var bool */ private $debug = false; /** * Inlines the given CSS into the existing HTML. * * @param string $css the CSS to inline, must be UTF-8-encoded * * @return $this * * @throws ParseException in debug mode, if an invalid selector is encountered * @throws \RuntimeException * in debug mode, if an internal PCRE error occurs * or `CssSelectorConverter::toXPath` returns an invalid XPath expression * @throws \UnexpectedValueException * if a selector query result includes a node which is not a `DOMElement` */ public function inlineCss(string $css = ''): self { $this->clearAllCaches(); $this->purgeVisitedNodes(); $this->normalizeStyleAttributesOfAllNodes(); $combinedCss = $css; // grab any existing style blocks from the HTML and append them to the existing CSS // (these blocks should be appended so as to have precedence over conflicting styles in the existing CSS) if ($this->isStyleBlocksParsingEnabled) { $combinedCss .= $this->getCssFromAllStyleNodes(); } $parsedCss = new CssDocument($combinedCss, $this->debug); $excludedNodes = $this->getNodesToExclude(); $cssRules = $this->collateCssRules($parsedCss); foreach ($cssRules['inlinable'] as $cssRule) { foreach ($this->querySelectorAll($cssRule['selector']) as $node) { if (\in_array($node, $excludedNodes, true)) { continue; } $this->copyInlinableCssToStyleAttribute($this->ensureNodeIsElement($node), $cssRule); } } if ($this->isInlineStyleAttributesParsingEnabled) { $this->fillStyleAttributesWithMergedStyles(); } $this->removeImportantAnnotationFromAllInlineStyles(); $this->determineMatchingUninlinableCssRules($cssRules['uninlinable']); $this->copyUninlinableCssToStyleNode($parsedCss); return $this; } /** * Disables the parsing of inline styles. * * @return $this */ public function disableInlineStyleAttributesParsing(): self { $this->isInlineStyleAttributesParsingEnabled = false; return $this; } /** * Disables the parsing of `<style>` blocks. * * @return $this */ public function disableStyleBlocksParsing(): self { $this->isStyleBlocksParsingEnabled = false; return $this; } /** * Marks a media query type to keep. * * @param string $mediaName the media type name, e.g., "braille" * * @return $this */ public function addAllowedMediaType(string $mediaName): self { $this->allowedMediaTypes[$mediaName] = true; return $this; } /** * Drops a media query type from the allowed list. * * @param string $mediaName the tag name, e.g., "braille" * * @return $this */ public function removeAllowedMediaType(string $mediaName): self { if (isset($this->allowedMediaTypes[$mediaName])) { unset($this->allowedMediaTypes[$mediaName]); } return $this; } /** * Adds a selector to exclude nodes from emogrification. * * Any nodes that match the selector will not have their style altered. * * @param string $selector the selector to exclude, e.g., ".editor" * * @return $this */ public function addExcludedSelector(string $selector): self { $this->excludedSelectors[$selector] = true; return $this; } /** * No longer excludes the nodes matching this selector from emogrification. * * @param string $selector the selector to no longer exclude, e.g., ".editor" * * @return $this */ public function removeExcludedSelector(string $selector): self { if (isset($this->excludedSelectors[$selector])) { unset($this->excludedSelectors[$selector]); } return $this; } /** * Adds a selector to exclude CSS selector from emogrification. * * @param non-empty-string $selector the selector to exclude, e.g., `.editor` * * @return $this */ public function addExcludedCssSelector(string $selector): self { $this->excludedCssSelectors[$selector] = true; return $this; } /** * No longer excludes the CSS selector from emogrification. * * @param non-empty-string $selector the selector to no longer exclude, e.g., `.editor` * * @return $this */ public function removeExcludedCssSelector(string $selector): self { if (isset($this->excludedCssSelectors[$selector])) { unset($this->excludedCssSelectors[$selector]); } return $this; } /** * Sets the debug mode. * * @param bool $debug set to true to enable debug mode * * @return $this */ public function setDebug(bool $debug): self { $this->debug = $debug; return $this; } /** * Gets the array of selectors present in the CSS provided to `inlineCss()` for which the declarations could not be * applied as inline styles, but which may affect elements in the HTML. The relevant CSS will have been placed in a * `<style>` element. The selectors may include those used within `@media` rules or those involving dynamic * pseudo-classes (such as `:hover`) or pseudo-elements (such as `::after`). * * @return array<array-key, string> * * @throws \BadMethodCallException if `inlineCss` has not been called first */ public function getMatchingUninlinableSelectors(): array { return \array_column($this->getMatchingUninlinableCssRules(), 'selector'); } /** * @return array<array-key, array{ * media: string, * selector: string, * hasUnmatchablePseudo: bool, * declarationsBlock: string, * line: int * }> * * @throws \BadMethodCallException if `inlineCss` has not been called first */ private function getMatchingUninlinableCssRules(): array { if (!\is_array($this->matchingUninlinableCssRules)) { throw new \BadMethodCallException('inlineCss must be called first', 1568385221); } return $this->matchingUninlinableCssRules; } /** * Clears all caches. */ private function clearAllCaches(): void { $this->caches = [ self::CACHE_KEY_SELECTOR => [], self::CACHE_KEY_COMBINED_STYLES => [], ]; } /** * Purges the visited nodes. */ private function purgeVisitedNodes(): void { $this->visitedNodes = []; $this->styleAttributesForNodes = []; } /** * Parses the document and normalizes all existing CSS attributes. * This changes 'DISPLAY: none' to 'display: none'. * We wouldn't have to do this if DOMXPath supported XPath 2.0. * Also stores a reference of nodes with existing inline styles so we don't overwrite them. */ private function normalizeStyleAttributesOfAllNodes(): void { /** @var \DOMElement $node */ foreach ($this->getAllNodesWithStyleAttribute() as $node) { if ($this->isInlineStyleAttributesParsingEnabled) { $this->normalizeStyleAttributes($node); } // Remove style attribute in every case, so we can add them back (if inline style attributes // parsing is enabled) to the end of the style list, thus keeping the right priority of CSS rules; // else original inline style rules may remain at the beginning of the final inline style definition // of a node, which may give not the desired results $node->removeAttribute('style'); } } /** * Returns a list with all DOM nodes that have a style attribute. * * @return \DOMNodeList * * @throws \RuntimeException */ private function getAllNodesWithStyleAttribute(): \DOMNodeList { $query = '//*[@style]'; $matches = $this->getXPath()->query($query); if (!$matches instanceof \DOMNodeList) { throw new \RuntimeException('XPatch query failed: ' . $query, 1618577797); } return $matches; } /** * Normalizes the value of the "style" attribute and saves it. * * @param \DOMElement $node */ private function normalizeStyleAttributes(\DOMElement $node): void { $declarationBlockParser = new DeclarationBlockParser(); $normalizedOriginalStyle = (new Preg())->throwExceptions($this->debug)->replaceCallback( '/-{0,2}+[_a-zA-Z][\\w\\-]*+(?=:)/S', /** @param array<array-key, string> $propertyNameMatches */ static function (array $propertyNameMatches) use ($declarationBlockParser): string { return $declarationBlockParser->normalizePropertyName($propertyNameMatches[0]); }, $node->getAttribute('style') ); // In order to not overwrite existing style attributes in the HTML, we have to save the original HTML styles. $nodePath = $node->getNodePath(); if (\is_string($nodePath) && !isset($this->styleAttributesForNodes[$nodePath])) { $this->styleAttributesForNodes[$nodePath] = $declarationBlockParser->parse($normalizedOriginalStyle); $this->visitedNodes[$nodePath] = $node; } $node->setAttribute('style', $normalizedOriginalStyle); } /** * Returns CSS content. * * @return string */ private function getCssFromAllStyleNodes(): string { $styleNodes = $this->getXPath()->query('//style'); if ($styleNodes === false) { return ''; } $css = ''; foreach ($styleNodes as $styleNode) { if (\is_string($styleNode->nodeValue)) { $css .= "\n\n" . $styleNode->nodeValue; } $parentNode = $styleNode->parentNode; if ($parentNode instanceof \DOMNode) { $parentNode->removeChild($styleNode); } } return $css; } /** * Find the nodes that are not to be emogrified. * * @return list<\DOMElement> * * @throws ParseException in debug mode, if an invalid selector is encountered * @throws \RuntimeException in debug mode, if `CssSelectorConverter::toXPath` returns an invalid XPath expression * @throws \UnexpectedValueException if the selector query result includes a node which is not a `DOMElement` */ private function getNodesToExclude(): array { $excludedNodes = []; foreach (\array_keys($this->excludedSelectors) as $selectorToExclude) { foreach ($this->querySelectorAll($selectorToExclude) as $node) { $excludedNodes[] = $this->ensureNodeIsElement($node); } } return $excludedNodes; } /** * @param array{}|array{alwaysThrowParseException: bool} $options * This is an array of option values to control behaviour: * - `QSA_ALWAYS_THROW_PARSE_EXCEPTION` - `bool` - throw any `ParseException` regardless of debug setting. * * @return \DOMNodeList<\DOMNode> the HTML elements that match the provided CSS `$selectors` * * @throws ParseException * in debug mode (or with `QSA_ALWAYS_THROW_PARSE_EXCEPTION` option), if an invalid selector is encountered * @throws \RuntimeException in debug mode, if `CssSelectorConverter::toXPath` returns an invalid XPath expression */ private function querySelectorAll(string $selectors, array $options = []): \DOMNodeList { try { $result = $this->getXPath()->query($this->getCssSelectorConverter()->toXPath($selectors)); if ($result === false) { throw new \RuntimeException('query failed with selector \'' . $selectors . '\'', 1726533051); } return $result; } catch (ParseException $exception) { $alwaysThrowParseException = $options[self::QSA_ALWAYS_THROW_PARSE_EXCEPTION] ?? false; if ($this->debug || $alwaysThrowParseException) { throw $exception; } return new \DOMNodeList(); } catch (\RuntimeException $exception) { if ( $this->debug ) { throw $exception; } // `RuntimeException` indicates a bug in CssSelector so pass the message to the error handler. \trigger_error($exception->getMessage()); return new \DOMNodeList(); } } /** * @throws \UnexpectedValueException if `$node` is not a `DOMElement` */ private function ensureNodeIsElement(\DOMNode $node): \DOMElement { if (!$node instanceof \DOMElement) { $path = $node->getNodePath() ?? '$node'; throw new \UnexpectedValueException($path . ' is not a DOMElement.', 1617975914); } return $node; } /** * @return CssSelectorConverter */ private function getCssSelectorConverter(): CssSelectorConverter { if (!$this->cssSelectorConverter instanceof CssSelectorConverter) { $this->cssSelectorConverter = new CssSelectorConverter(); } return $this->cssSelectorConverter; } /** * Collates the individual rules from a `CssDocument` object. * * @param CssDocument $parsedCss * * @return array<string, array<array-key, array{ * media: string, * selector: string, * hasUnmatchablePseudo: bool, * declarationsBlock: string, * line: int * }>> * This 2-entry array has the key "inlinable" containing rules which can be inlined as `style` attributes * and the key "uninlinable" containing rules which cannot. Each value is an array of sub-arrays with the * following keys: * - "media" (the media query string, e.g. "@media screen and (max-width: 480px)", * or an empty string if not from a `@media` rule); * - "selector" (the CSS selector, e.g., "*" or "header h1"); * - "hasUnmatchablePseudo" (`true` if that selector contains pseudo-elements or dynamic pseudo-classes such * that the declarations cannot be applied inline); * - "declarationsBlock" (the semicolon-separated CSS declarations for that selector, * e.g., `color: red; height: 4px;`); * - "line" (the line number, e.g. 42). */ private function collateCssRules(CssDocument $parsedCss): array { $matches = $parsedCss->getStyleRulesData(\array_keys($this->allowedMediaTypes)); $preg = (new Preg())->throwExceptions($this->debug); $cssRules = [ 'inlinable' => [], 'uninlinable' => [], ]; foreach ($matches as $key => $cssRule) { if (!$cssRule->hasAtLeastOneDeclaration()) { continue; } $mediaQuery = $cssRule->getContainingAtRule(); $declarationsBlock = $cssRule->getDeclarationAsText(); $selectors = $cssRule->getSelectors(); // Maybe exclude CSS selectors if (\count($this->excludedCssSelectors) > 0) { // Normalize spaces, line breaks & tabs $selectorsNormalized = \array_map(static function (string $selector) use ($preg): string { return $preg->replace('@\\s++@u', ' ', $selector); }, $selectors); $selectors = \array_filter($selectorsNormalized, function (string $selector): bool { return !isset($this->excludedCssSelectors[$selector]); }); } foreach ($selectors as $selector) { // don't process pseudo-elements and behavioral (dynamic) pseudo-classes; // only allow structural pseudo-classes $hasPseudoElement = \strpos($selector, '::') !== false; $hasUnmatchablePseudo = $hasPseudoElement || $this->hasUnsupportedPseudoClass($selector); $parsedCssRule = [ 'media' => $mediaQuery, 'selector' => $selector, 'hasUnmatchablePseudo' => $hasUnmatchablePseudo, 'declarationsBlock' => $declarationsBlock, // keep track of where it appears in the file, since order is important 'line' => $key, ]; $ruleType = (!$cssRule->hasContainingAtRule() && !$hasUnmatchablePseudo) ? 'inlinable' : 'uninlinable'; $cssRules[$ruleType][] = $parsedCssRule; } } \usort( $cssRules['inlinable'], /** * @param array{selector: string, line: int, ...} $first * @param array{selector: string, line: int, ...} $second */ function (array $first, array $second): int { return $this->sortBySelectorPrecedence($first, $second); } ); return $cssRules; } /** * Tests if a selector contains a pseudo-class which would mean it cannot be converted to an XPath expression for * inlining CSS declarations. * * Any pseudo class that does not match {@see PSEUDO_CLASS_MATCHER} cannot be converted. Additionally, `...of-type` * pseudo-classes cannot be converted if they are not associated with a type selector. * * @param string $selector * * @return bool */ private function hasUnsupportedPseudoClass(string $selector): bool { $preg = (new Preg())->throwExceptions($this->debug); if ($preg->match('/:(?!' . self::PSEUDO_CLASS_MATCHER . ')[\\w\\-]/i', $selector) !== 0) { return true; } if ($preg->match('/:(?:' . self::OF_TYPE_PSEUDO_CLASS_MATCHER . ')/i', $selector) === 0) { return false; } foreach ($preg->split('/' . self::COMBINATOR_MATCHER . '/', $selector) as $selectorPart) { if ($this->selectorPartHasUnsupportedOfTypePseudoClass($selectorPart)) { return true; } } return false; } /** * Tests if part of a selector contains an `...of-type` pseudo-class such that it cannot be converted to an XPath * expression. * * @param string $selectorPart part of a selector which has been split up at combinators * * @return bool `true` if the selector part does not have a type but does have an `...of-type` pseudo-class */ private function selectorPartHasUnsupportedOfTypePseudoClass(string $selectorPart): bool { $preg = (new Preg())->throwExceptions($this->debug); if ($preg->match('/^[\\w\\-]/', $selectorPart) !== 0) { return false; } return $preg->match('/:(?:' . self::OF_TYPE_PSEUDO_CLASS_MATCHER . ')/i', $selectorPart) !== 0; } /** * @param array{selector: string, line: int, ...} $first * @param array{selector: string, line: int, ...} $second * * @return int */ private function sortBySelectorPrecedence(array $first, array $second): int { $precedenceOfFirst = $this->getCssSelectorPrecedence($first['selector']); $precedenceOfSecond = $this->getCssSelectorPrecedence($second['selector']); // We want these sorted in ascending order so selectors with lesser precedence get processed first and // selectors with greater precedence get sorted last. $precedenceForEquals = $first['line'] < $second['line'] ? -1 : 1; $precedenceForNotEquals = $precedenceOfFirst < $precedenceOfSecond ? -1 : 1; return ($precedenceOfFirst === $precedenceOfSecond) ? $precedenceForEquals : $precedenceForNotEquals; } /** * @param string $selector * * @return int */ private function getCssSelectorPrecedence(string $selector): int { $selectorKey = $selector; if (isset($this->caches[self::CACHE_KEY_SELECTOR][$selectorKey])) { return $this->caches[self::CACHE_KEY_SELECTOR][$selectorKey]; } $preg = (new Preg())->throwExceptions($this->debug); $precedence = 0; foreach ($this->selectorPrecedenceMatchers as $matcher => $value) { if (\trim($selector) === '') { break; } $count = 0; $selector = $preg->replace('/' . $matcher . '\\w+/', '', $selector, -1, $count); $precedence += ($value * $count); } $this->caches[self::CACHE_KEY_SELECTOR][$selectorKey] = $precedence; return $precedence; } /** * Copies $cssRule into the style attribute of $node. * * Note: This method does not check whether $cssRule matches $node. * * @param \DOMElement $node * @param array{ * media: string, * selector: string, * hasUnmatchablePseudo: bool, * declarationsBlock: string, * line: int * } $cssRule */ private function copyInlinableCssToStyleAttribute(\DOMElement $node, array $cssRule): void { $declarationsBlock = $cssRule['declarationsBlock']; $declarationBlockParser = new DeclarationBlockParser(); $newStyleDeclarations = $declarationBlockParser->parse($declarationsBlock); if ($newStyleDeclarations === []) { return; } // if it has a style attribute, get it, process it, and append (overwrite) new stuff if ($node->hasAttribute('style')) { // break it up into an associative array $oldStyleDeclarations = $declarationBlockParser->parse($node->getAttribute('style')); } else { $oldStyleDeclarations = []; } $node->setAttribute( 'style', $this->generateStyleStringFromDeclarationsArrays($oldStyleDeclarations, $newStyleDeclarations) ); } /** * This method merges old or existing name/value array with new name/value array * and then generates a string of the combined style suitable for placing inline. * This becomes the single point for CSS string generation allowing for consistent * CSS output no matter where the CSS originally came from. * * @param array<string, string> $oldStyles * @param array<string, string> $newStyles * * @return string * * @throws \UnexpectedValueException if an empty property name is encountered (which should not happen) */ private function generateStyleStringFromDeclarationsArrays(array $oldStyles, array $newStyles): string { $cacheKey = \serialize([$oldStyles, $newStyles]); if (isset($this->caches[self::CACHE_KEY_COMBINED_STYLES][$cacheKey])) { return $this->caches[self::CACHE_KEY_COMBINED_STYLES][$cacheKey]; } // Unset the overridden styles to preserve order, important if shorthand and individual properties are mixed foreach ($oldStyles as $attributeName => $attributeValue) { if (!isset($newStyles[$attributeName])) { continue; } $newAttributeValue = $newStyles[$attributeName]; if ( $this->attributeValueIsImportant($attributeValue) && !$this->attributeValueIsImportant($newAttributeValue) ) { unset($newStyles[$attributeName]); } else { unset($oldStyles[$attributeName]); } } $combinedStyles = \array_merge($oldStyles, $newStyles); $declarationBlockParser = new DeclarationBlockParser(); $style = ''; foreach ($combinedStyles as $attributeName => $attributeValue) { $trimmedAttributeName = \trim($attributeName); if ($trimmedAttributeName === '') { throw new \UnexpectedValueException('An empty property name was encountered.', 1727046078); } $propertyName = $declarationBlockParser->normalizePropertyName($trimmedAttributeName); $propertyValue = \trim($attributeValue); $style .= $propertyName . ': ' . $propertyValue . '; '; } $trimmedStyle = \rtrim($style); $this->caches[self::CACHE_KEY_COMBINED_STYLES][$cacheKey] = $trimmedStyle; return $trimmedStyle; } /** * Checks whether $attributeValue is marked as !important. * * @param string $attributeValue * * @return bool */ private function attributeValueIsImportant(string $attributeValue): bool { return (new Preg())->throwExceptions($this->debug)->match('/!\\s*+important$/i', $attributeValue) !== 0; } /** * Merges styles from styles attributes and style nodes and applies them to the attribute nodes */ private function fillStyleAttributesWithMergedStyles(): void { $declarationBlockParser = new DeclarationBlockParser(); foreach ($this->styleAttributesForNodes as $nodePath => $styleAttributesForNode) { $node = $this->visitedNodes[$nodePath]; $currentStyleAttributes = $declarationBlockParser->parse($node->getAttribute('style')); $node->setAttribute( 'style', $this->generateStyleStringFromDeclarationsArrays( $currentStyleAttributes, $styleAttributesForNode ) ); } } /** * Searches for all nodes with a style attribute and removes the "!important" annotations out of * the inline style declarations, eventually by rearranging declarations. * * @throws \RuntimeException */ private function removeImportantAnnotationFromAllInlineStyles(): void { /** @var \DOMElement $node */ foreach ($this->getAllNodesWithStyleAttribute() as $node) { $this->removeImportantAnnotationFromNodeInlineStyle($node); } } /** * Removes the "!important" annotations out of the inline style declarations, * eventually by rearranging declarations. * Rearranging needed when !important shorthand properties are followed by some of their * not !important expanded-version properties. * For example "font: 12px serif !important; font-size: 13px;" must be reordered * to "font-size: 13px; font: 12px serif;" in order to remain correct. * * @param \DOMElement $node * * @throws \RuntimeException */ private function removeImportantAnnotationFromNodeInlineStyle(\DOMElement $node): void { $style = $node->getAttribute('style'); $inlineStyleDeclarations = (new DeclarationBlockParser())->parse((bool) $style ? $style : ''); /** @var array<string, string> $regularStyleDeclarations */ $regularStyleDeclarations = []; /** @var array<string, string> $importantStyleDeclarations */ $importantStyleDeclarations = []; foreach ($inlineStyleDeclarations as $property => $value) { if ($this->attributeValueIsImportant($value)) { $importantStyleDeclarations[$property] = (new Preg())->throwExceptions($this->debug)->replace('/\\s*+!\\s*+important$/i', '', $value); } else { $regularStyleDeclarations[$property] = $value; } } $inlineStyleDeclarationsInNewOrder = \array_merge($regularStyleDeclarations, $importantStyleDeclarations); $node->setAttribute( 'style', $this->generateStyleStringFromSingleDeclarationsArray($inlineStyleDeclarationsInNewOrder) ); } /** * Generates a CSS style string suitable to be used inline from the $styleDeclarations property => value array. * * @param array<string, string> $styleDeclarations * * @return string */ private function generateStyleStringFromSingleDeclarationsArray(array $styleDeclarations): string { return $this->generateStyleStringFromDeclarationsArrays([], $styleDeclarations); } /** * Determines which of `$cssRules` actually apply to `$this->domDocument`, and sets them in * `$this->matchingUninlinableCssRules`. * * @param array<array-key, array{ * media: string, * selector: string, * hasUnmatchablePseudo: bool, * declarationsBlock: string, * line: int * }> $cssRules * the "uninlinable" array of CSS rules returned by `collateCssRules` */ private function determineMatchingUninlinableCssRules(array $cssRules): void { $this->matchingUninlinableCssRules = \array_filter( $cssRules, function (array $cssRule): bool { return $this->existsMatchForSelectorInCssRule($cssRule); } ); } /** * Checks whether there is at least one matching element for the CSS selector contained in the `selector` element * of the provided CSS rule. * * Any dynamic pseudo-classes will be assumed to apply. If the selector matches a pseudo-element, * it will test for a match with its originating element. * * @param array{ * media: string, * selector: string, * hasUnmatchablePseudo: bool, * declarationsBlock: string, * line: int * } $cssRule * * @return bool * * @throws ParseException */ private function existsMatchForSelectorInCssRule(array $cssRule): bool { $selector = $cssRule['selector']; if ($cssRule['hasUnmatchablePseudo']) { $selector = $this->removeUnmatchablePseudoComponents($selector); } return $this->existsMatchForCssSelector($selector); } /** * Checks whether there is at least one matching element for $cssSelector. * When not in debug mode, it returns true also for invalid selectors (because they may be valid, * just not implemented/recognized yet by Emogrifier). * * @param string $cssSelector * * @return bool * * @throws ParseException in debug mode, if an invalid selector is encountered * @throws \RuntimeException in debug mode, if `CssSelectorConverter::toXPath` returns an invalid XPath expression */ private function existsMatchForCssSelector(string $cssSelector): bool { try { $nodesMatchingSelector = $this->querySelectorAll($cssSelector, [self::QSA_ALWAYS_THROW_PARSE_EXCEPTION => true]); } catch (ParseException $e) { if ($this->debug) { throw $e; } return true; } return $nodesMatchingSelector->length !== 0; } /** * Removes pseudo-elements and dynamic pseudo-classes from a CSS selector, replacing them with "*" if necessary. * If such a pseudo-component is within the argument of `:not`, the entire `:not` component is removed or replaced. * * @param string $selector * * @return string * selector which will match the relevant DOM elements if the pseudo-classes are assumed to apply, or in the * case of pseudo-elements will match their originating element */ private function removeUnmatchablePseudoComponents(string $selector): string { $preg = (new Preg())->throwExceptions($this->debug); // The regex allows nested brackets via `(?2)`. // A space is temporarily prepended because the callback can't determine if the match was at the very start. $selectorWithoutNots = \ltrim((new Preg())->throwExceptions($this->debug)->replaceCallback( '/([\\s>+~]?+):not(\\([^()]*+(?:(?2)[^()]*+)*+\\))/i', /** @param array<array-key, string> $matches */ function (array $matches): string { return $this->replaceUnmatchableNotComponent($matches); }, ' ' . $selector )); $selectorWithoutUnmatchablePseudoComponents = $this->removeSelectorComponents( ':(?!' . self::PSEUDO_CLASS_MATCHER . '):?+[\\w\\-]++(?:\\([^\\)]*+\\))?+', $selectorWithoutNots ); if ( $preg->match( '/:(?:' . self::OF_TYPE_PSEUDO_CLASS_MATCHER . ')/i', $selectorWithoutUnmatchablePseudoComponents ) === 0 ) { return $selectorWithoutUnmatchablePseudoComponents; } return \implode('', \array_map( function (string $selectorPart): string { return $this->removeUnsupportedOfTypePseudoClasses($selectorPart); }, $preg->split( '/(' . self::COMBINATOR_MATCHER . ')/', $selectorWithoutUnmatchablePseudoComponents, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY ) )); } /** * Helps `removeUnmatchablePseudoComponents()` replace or remove a selector `:not(...)` component if its argument * contains pseudo-elements or dynamic pseudo-classes. * * @param array<array-key, string> $matches array of elements matched by the regular expression * * @return string * the full match if there were no unmatchable pseudo components within; otherwise, any preceding combinator * followed by "*", or an empty string if there was no preceding combinator */ private function replaceUnmatchableNotComponent(array $matches): string { [$notComponentWithAnyPrecedingCombinator, $anyPrecedingCombinator, $notArgumentInBrackets] = $matches; if ($this->hasUnsupportedPseudoClass($notArgumentInBrackets)) { return $anyPrecedingCombinator !== '' ? $anyPrecedingCombinator . '*' : ''; } return $notComponentWithAnyPrecedingCombinator; } /** * Removes components from a CSS selector, replacing them with "*" if necessary. * * @param string $matcher regular expression part to match the components to remove * @param string $selector * * @return string * selector which will match the relevant DOM elements if the removed components are assumed to apply (or in * the case of pseudo-elements will match their originating element) */ private function removeSelectorComponents(string $matcher, string $selector): string { return (new Preg())->throwExceptions($this->debug)->replace( ['/([\\s>+~]|^)' . $matcher . '/i', '/' . $matcher . '/i'], ['$1*', ''], $selector ); } /** * Removes any `...-of-type` pseudo-classes from part of a CSS selector, if it does not have a type, replacing them * with "*" if necessary. * * @param string $selectorPart part of a selector which has been split up at combinators * * @return string * selector part which will match the relevant DOM elements if the pseudo-classes are assumed to apply */ private function removeUnsupportedOfTypePseudoClasses(string $selectorPart): string { if (!$this->selectorPartHasUnsupportedOfTypePseudoClass($selectorPart)) { return $selectorPart; } return $this->removeSelectorComponents( ':(?:' . self::OF_TYPE_PSEUDO_CLASS_MATCHER . ')(?:\\([^\\)]*+\\))?+', $selectorPart ); } /** * Applies `$this->matchingUninlinableCssRules` to `$this->domDocument` by placing them as CSS in a `<style>` * element. * If there are no uninlinable CSS rules to copy there, a `<style>` element will be created containing only the * applicable at-rules from `$parsedCss`. * If there are none of either, an empty `<style>` element will not be created. * * @param CssDocument $parsedCss * This may contain various at-rules whose content `CssInliner` does not currently attempt to inline or * process in any other way, such as `@import`, `@font-face`, `@keyframes`, etc., and which should precede * the processed but found-to-be-uninlinable CSS placed in the `<style>` element. * Note that `CssInliner` processes `@media` rules so that they can be ordered correctly with respect to * other uninlinable rules; these will not be duplicated from `$parsedCss`. */ private function copyUninlinableCssToStyleNode(CssDocument $parsedCss): void { $css = $parsedCss->renderNonConditionalAtRules(); // avoid including unneeded class dependency if there are no rules if ($this->getMatchingUninlinableCssRules() !== []) { $cssConcatenator = new CssConcatenator(); foreach ($this->getMatchingUninlinableCssRules() as $cssRule) { $cssConcatenator->append([$cssRule['selector']], $cssRule['declarationsBlock'], $cssRule['media']); } $css .= $cssConcatenator->getCss(); } // avoid adding empty style element if ($css !== '') { $this->addStyleElementToDocument($css); } } /** * Adds a style element with $css to $this->domDocument. * * This method is protected to allow overriding. * * @see https://github.com/MyIntervals/emogrifier/issues/103 * * @param string $css */ protected function addStyleElementToDocument(string $css): void { $domDocument = $this->getDomDocument(); $styleElement = $domDocument->createElement('style', $css); $styleAttribute = $domDocument->createAttribute('type'); $styleAttribute->value = 'text/css'; $styleElement->appendChild($styleAttribute); $headElement = $this->getHeadElement(); $headElement->appendChild($styleElement); } /** * Returns the HEAD element. * * This method assumes that there always is a HEAD element. * * @return \DOMElement * * @throws \UnexpectedValueException */ private function getHeadElement(): \DOMElement { $node = $this->getDomDocument()->getElementsByTagName('head')->item(0); if (!$node instanceof \DOMElement) { throw new \UnexpectedValueException('There is no HEAD element. This should never happen.', 1617923227); } return $node; } } packages/Detection/MobileDetect.php 0000644 00000232635 15217646626 0013343 0 ustar 00 <?php /** * Mobile Detect Library * Motto: "Every business should have a mobile detection script to detect mobile readers" * * Mobile_Detect is a lightweight PHP class for detecting mobile devices (including tablets). * It uses the User-Agent string combined with specific HTTP headers to detect the mobile environment. * * Homepage: http://mobiledetect.net * GitHub: https://github.com/serbanghita/Mobile-Detect * README: https://github.com/serbanghita/Mobile-Detect/blob/master/README.md * CONTRIBUTING: https://github.com/serbanghita/Mobile-Detect/blob/master/docs/CONTRIBUTING.md * KNOWN LIMITATIONS: https://github.com/serbanghita/Mobile-Detect/blob/master/docs/KNOWN_LIMITATIONS.md * EXAMPLES: https://github.com/serbanghita/Mobile-Detect/wiki/Code-examples * * @license https://github.com/serbanghita/Mobile-Detect/blob/master/LICENSE * @author Serban Ghita <serbanghita@gmail.com> (since 2012) * @author Nick Ilyin <nick.ilyin@gmail.com> * @author: Victor Stanciu <vic.stanciu@gmail.com> (original author) * * @version 3.74.3 */ namespace Automattic\WooCommerce\Vendor\Detection; use BadMethodCallException; /** * Auto-generated isXXXX() magic methods. * php export/dump_magic_methods.php * * @method bool isiPhone() * @method bool isBlackBerry() * @method bool isPixel() * @method bool isHTC() * @method bool isNexus() * @method bool isDell() * @method bool isMotorola() * @method bool isSamsung() * @method bool isLG() * @method bool isSony() * @method bool isAsus() * @method bool isXiaomi() * @method bool isNokiaLumia() * @method bool isMicromax() * @method bool isPalm() * @method bool isVertu() * @method bool isPantech() * @method bool isFly() * @method bool isWiko() * @method bool isiMobile() * @method bool isSimValley() * @method bool isWolfgang() * @method bool isAlcatel() * @method bool isNintendo() * @method bool isAmoi() * @method bool isINQ() * @method bool isOnePlus() * @method bool isGenericPhone() * @method bool isiPad() * @method bool isNexusTablet() * @method bool isGoogleTablet() * @method bool isSamsungTablet() * @method bool isKindle() * @method bool isSurfaceTablet() * @method bool isHPTablet() * @method bool isAsusTablet() * @method bool isBlackBerryTablet() * @method bool isHTCtablet() * @method bool isMotorolaTablet() * @method bool isNookTablet() * @method bool isAcerTablet() * @method bool isToshibaTablet() * @method bool isLGTablet() * @method bool isFujitsuTablet() * @method bool isPrestigioTablet() * @method bool isLenovoTablet() * @method bool isDellTablet() * @method bool isYarvikTablet() * @method bool isMedionTablet() * @method bool isArnovaTablet() * @method bool isIntensoTablet() * @method bool isIRUTablet() * @method bool isMegafonTablet() * @method bool isEbodaTablet() * @method bool isAllViewTablet() * @method bool isArchosTablet() * @method bool isAinolTablet() * @method bool isNokiaLumiaTablet() * @method bool isSonyTablet() * @method bool isPhilipsTablet() * @method bool isCubeTablet() * @method bool isCobyTablet() * @method bool isMIDTablet() * @method bool isMSITablet() * @method bool isSMiTTablet() * @method bool isRockChipTablet() * @method bool isFlyTablet() * @method bool isbqTablet() * @method bool isHuaweiTablet() * @method bool isNecTablet() * @method bool isPantechTablet() * @method bool isBronchoTablet() * @method bool isVersusTablet() * @method bool isZyncTablet() * @method bool isPositivoTablet() * @method bool isNabiTablet() * @method bool isKoboTablet() * @method bool isDanewTablet() * @method bool isTexetTablet() * @method bool isPlaystationTablet() * @method bool isTrekstorTablet() * @method bool isPyleAudioTablet() * @method bool isAdvanTablet() * @method bool isDanyTechTablet() * @method bool isGalapadTablet() * @method bool isMicromaxTablet() * @method bool isKarbonnTablet() * @method bool isAllFineTablet() * @method bool isPROSCANTablet() * @method bool isYONESTablet() * @method bool isChangJiaTablet() * @method bool isGUTablet() * @method bool isPointOfViewTablet() * @method bool isOvermaxTablet() * @method bool isHCLTablet() * @method bool isDPSTablet() * @method bool isVistureTablet() * @method bool isCrestaTablet() * @method bool isMediatekTablet() * @method bool isConcordeTablet() * @method bool isGoCleverTablet() * @method bool isModecomTablet() * @method bool isVoninoTablet() * @method bool isECSTablet() * @method bool isStorexTablet() * @method bool isVodafoneTablet() * @method bool isEssentielBTablet() * @method bool isRossMoorTablet() * @method bool isiMobileTablet() * @method bool isTolinoTablet() * @method bool isAudioSonicTablet() * @method bool isAMPETablet() * @method bool isSkkTablet() * @method bool isTecnoTablet() * @method bool isJXDTablet() * @method bool isiJoyTablet() * @method bool isFX2Tablet() * @method bool isXoroTablet() * @method bool isViewsonicTablet() * @method bool isVerizonTablet() * @method bool isOdysTablet() * @method bool isCaptivaTablet() * @method bool isIconbitTablet() * @method bool isTeclastTablet() * @method bool isOndaTablet() * @method bool isJaytechTablet() * @method bool isBlaupunktTablet() * @method bool isDigmaTablet() * @method bool isEvolioTablet() * @method bool isLavaTablet() * @method bool isAocTablet() * @method bool isMpmanTablet() * @method bool isCelkonTablet() * @method bool isWolderTablet() * @method bool isMediacomTablet() * @method bool isMiTablet() * @method bool isNibiruTablet() * @method bool isNexoTablet() * @method bool isLeaderTablet() * @method bool isUbislateTablet() * @method bool isPocketBookTablet() * @method bool isKocasoTablet() * @method bool isHisenseTablet() * @method bool isHudl() * @method bool isTelstraTablet() * @method bool isGenericTablet() * @method bool isAndroidOS() * @method bool isBlackBerryOS() * @method bool isPalmOS() * @method bool isSymbianOS() * @method bool isWindowsMobileOS() * @method bool isWindowsPhoneOS() * @method bool isiOS() * @method bool isiPadOS() * @method bool isSailfishOS() * @method bool isMeeGoOS() * @method bool isMaemoOS() * @method bool isJavaOS() * @method bool iswebOS() * @method bool isbadaOS() * @method bool isBREWOS() * @method bool isChrome() * @method bool isDolfin() * @method bool isOpera() * @method bool isSkyfire() * @method bool isEdge() * @method bool isIE() * @method bool isFirefox() * @method bool isBolt() * @method bool isTeaShark() * @method bool isBlazer() * @method bool isSafari() * @method bool isWeChat() * @method bool isUCBrowser() * @method bool isbaiduboxapp() * @method bool isbaidubrowser() * @method bool isDiigoBrowser() * @method bool isMercury() * @method bool isObigoBrowser() * @method bool isNetFront() * @method bool isGenericBrowser() * @method bool isPaleMoon() * @method bool isWebKit() * @method bool isConsole() * @method bool isWatch() */ class MobileDetect { /** * A frequently used regular expression to extract version #s. * * @deprecated since version 2.6.9 */ const VER = '([\w._\+]+)'; /** * Stores the version number of the current release. */ const VERSION = '3.74.3'; /** * A type for the version() method indicating a string return value. */ const VERSION_TYPE_STRING = 'text'; /** * A type for the version() method indicating a float return value. */ const VERSION_TYPE_FLOAT = 'float'; /** * A cache for resolved matches * @var array */ protected array $cache = []; /** * The User-Agent HTTP header is stored in here. * @var string|null */ protected ?string $userAgent = null; /** * HTTP headers in the PHP-flavor. So HTTP_USER_AGENT and SERVER_SOFTWARE. * @var array */ protected array $httpHeaders = []; /** * CloudFront headers. E.g. CloudFront-Is-Desktop-Viewer, CloudFront-Is-Mobile-Viewer & CloudFront-Is-Tablet-Viewer. * @var array */ protected array $cloudfrontHeaders = []; /** * The matching Regex. * This is good for debug. * @var string|null */ protected ?string $matchingRegex = null; /** * The matches extracted from the regex expression. * This is good for debug. * * @var array|null */ protected ?array $matchesArray = null; /** * HTTP headers that trigger the 'isMobile' detection * to be true. * * @var array */ protected static array $mobileHeaders = [ 'HTTP_ACCEPT' => [ 'matches' => [ // Opera Mini // @reference: http://dev.opera.com/articles/view/opera-binary-markup-language/ 'application/x-obml2d', // BlackBerry devices. 'application/vnd.rim.html', 'text/vnd.wap.wml', 'application/vnd.wap.xhtml+xml' ]], 'HTTP_X_WAP_PROFILE' => null, 'HTTP_X_WAP_CLIENTID' => null, 'HTTP_WAP_CONNECTION' => null, 'HTTP_PROFILE' => null, // Reported by Opera on Nokia devices (eg. C3). 'HTTP_X_OPERAMINI_PHONE_UA' => null, 'HTTP_X_NOKIA_GATEWAY_ID' => null, 'HTTP_X_ORANGE_ID' => null, 'HTTP_X_VODAFONE_3GPDPCONTEXT' => null, 'HTTP_X_HUAWEI_USERID' => null, // Reported by Windows Smartphones. 'HTTP_UA_OS' => null, // Reported by Verizon, Vodafone proxy system. 'HTTP_X_MOBILE_GATEWAY' => null, // Seen this on HTC Sensation. SensationXE_Beats_Z715e. 'HTTP_X_ATT_DEVICEID' => null, // Seen this on a HTC. 'HTTP_UA_CPU' => ['matches' => ['ARM']], ]; /** * List of mobile devices (phones). * * @var array */ protected static array $phoneDevices = [ 'iPhone' => '\biPhone\b|\biPod\b', // |\biTunes 'BlackBerry' => 'BlackBerry|\bBB10\b|rim[0-9]+|\b(BBA100|BBB100|BBD100|BBE100|BBF100|STH100)\b-[0-9]+', 'Pixel' => '; \bPixel\b', 'HTC' => 'HTC|HTC.*(Sensation|Evo|Vision|Explorer|6800|8100|8900|A7272|S510e|C110e|Legend|Desire|T8282)|APX515CKT|Qtek9090|APA9292KT|HD_mini|Sensation.*Z710e|PG86100|Z715e|Desire.*(A8181|HD)|ADR6200|ADR6400L|ADR6425|001HT|Inspire 4G|Android.*\bEVO\b|T-Mobile G1|Z520m|Android [0-9.]+; Pixel', 'Nexus' => 'Nexus One|Nexus S|Galaxy.*Nexus|Android.*Nexus.*Mobile|Nexus 4|Nexus 5|Nexus 5X|Nexus 6', // @todo: Is 'Dell Streak' a tablet or a phone? ;) 'Dell' => 'Dell[;]? (Streak|Aero|Venue|Venue Pro|Flash|Smoke|Mini 3iX)|XCD28|XCD35|\b001DL\b|\b101DL\b|\bGS01\b', 'Motorola' => 'Motorola|DROIDX|DROID BIONIC|\bDroid\b.*Build|Android.*Xoom|HRI39|MOT-|A1260|A1680|A555|A853|A855|A953|A955|A956|Motorola.*ELECTRIFY|Motorola.*i1|i867|i940|MB200|MB300|MB501|MB502|MB508|MB511|MB520|MB525|MB526|MB611|MB612|MB632|MB810|MB855|MB860|MB861|MB865|MB870|ME501|ME502|ME511|ME525|ME600|ME632|ME722|ME811|ME860|ME863|ME865|MT620|MT710|MT716|MT720|MT810|MT870|MT917|Motorola.*TITANIUM|WX435|WX445|XT300|XT301|XT311|XT316|XT317|XT319|XT320|XT390|XT502|XT530|XT531|XT532|XT535|XT603|XT610|XT611|XT615|XT681|XT701|XT702|XT711|XT720|XT800|XT806|XT860|XT862|XT875|XT882|XT883|XT894|XT901|XT907|XT909|XT910|XT912|XT928|XT926|XT915|XT919|XT925|XT1021|\bMoto E\b|XT1068|XT1092|XT1052', 'Samsung' => '\bSamsung\b|SM-G950F|SM-G955F|SM-G9250|GT-19300|SGH-I337|BGT-S5230|GT-B2100|GT-B2700|GT-B2710|GT-B3210|GT-B3310|GT-B3410|GT-B3730|GT-B3740|GT-B5510|GT-B5512|GT-B5722|GT-B6520|GT-B7300|GT-B7320|GT-B7330|GT-B7350|GT-B7510|GT-B7722|GT-B7800|GT-C3010|GT-C3011|GT-C3060|GT-C3200|GT-C3212|GT-C3212I|GT-C3262|GT-C3222|GT-C3300|GT-C3300K|GT-C3303|GT-C3303K|GT-C3310|GT-C3322|GT-C3330|GT-C3350|GT-C3500|GT-C3510|GT-C3530|GT-C3630|GT-C3780|GT-C5010|GT-C5212|GT-C6620|GT-C6625|GT-C6712|GT-E1050|GT-E1070|GT-E1075|GT-E1080|GT-E1081|GT-E1085|GT-E1087|GT-E1100|GT-E1107|GT-E1110|GT-E1120|GT-E1125|GT-E1130|GT-E1160|GT-E1170|GT-E1175|GT-E1180|GT-E1182|GT-E1200|GT-E1210|GT-E1225|GT-E1230|GT-E1390|GT-E2100|GT-E2120|GT-E2121|GT-E2152|GT-E2220|GT-E2222|GT-E2230|GT-E2232|GT-E2250|GT-E2370|GT-E2550|GT-E2652|GT-E3210|GT-E3213|GT-I5500|GT-I5503|GT-I5700|GT-I5800|GT-I5801|GT-I6410|GT-I6420|GT-I7110|GT-I7410|GT-I7500|GT-I8000|GT-I8150|GT-I8160|GT-I8190|GT-I8320|GT-I8330|GT-I8350|GT-I8530|GT-I8700|GT-I8703|GT-I8910|GT-I9000|GT-I9001|GT-I9003|GT-I9010|GT-I9020|GT-I9023|GT-I9070|GT-I9082|GT-I9100|GT-I9103|GT-I9220|GT-I9250|GT-I9300|GT-I9305|GT-I9500|GT-I9505|GT-M3510|GT-M5650|GT-M7500|GT-M7600|GT-M7603|GT-M8800|GT-M8910|GT-N7000|GT-S3110|GT-S3310|GT-S3350|GT-S3353|GT-S3370|GT-S3650|GT-S3653|GT-S3770|GT-S3850|GT-S5210|GT-S5220|GT-S5229|GT-S5230|GT-S5233|GT-S5250|GT-S5253|GT-S5260|GT-S5263|GT-S5270|GT-S5300|GT-S5330|GT-S5350|GT-S5360|GT-S5363|GT-S5369|GT-S5380|GT-S5380D|GT-S5560|GT-S5570|GT-S5600|GT-S5603|GT-S5610|GT-S5620|GT-S5660|GT-S5670|GT-S5690|GT-S5750|GT-S5780|GT-S5830|GT-S5839|GT-S6102|GT-S6500|GT-S7070|GT-S7200|GT-S7220|GT-S7230|GT-S7233|GT-S7250|GT-S7500|GT-S7530|GT-S7550|GT-S7562|GT-S7710|GT-S8000|GT-S8003|GT-S8500|GT-S8530|GT-S8600|SCH-A310|SCH-A530|SCH-A570|SCH-A610|SCH-A630|SCH-A650|SCH-A790|SCH-A795|SCH-A850|SCH-A870|SCH-A890|SCH-A930|SCH-A950|SCH-A970|SCH-A990|SCH-I100|SCH-I110|SCH-I400|SCH-I405|SCH-I500|SCH-I510|SCH-I515|SCH-I600|SCH-I730|SCH-I760|SCH-I770|SCH-I830|SCH-I910|SCH-I920|SCH-I959|SCH-LC11|SCH-N150|SCH-N300|SCH-R100|SCH-R300|SCH-R351|SCH-R400|SCH-R410|SCH-T300|SCH-U310|SCH-U320|SCH-U350|SCH-U360|SCH-U365|SCH-U370|SCH-U380|SCH-U410|SCH-U430|SCH-U450|SCH-U460|SCH-U470|SCH-U490|SCH-U540|SCH-U550|SCH-U620|SCH-U640|SCH-U650|SCH-U660|SCH-U700|SCH-U740|SCH-U750|SCH-U810|SCH-U820|SCH-U900|SCH-U940|SCH-U960|SCS-26UC|SGH-A107|SGH-A117|SGH-A127|SGH-A137|SGH-A157|SGH-A167|SGH-A177|SGH-A187|SGH-A197|SGH-A227|SGH-A237|SGH-A257|SGH-A437|SGH-A517|SGH-A597|SGH-A637|SGH-A657|SGH-A667|SGH-A687|SGH-A697|SGH-A707|SGH-A717|SGH-A727|SGH-A737|SGH-A747|SGH-A767|SGH-A777|SGH-A797|SGH-A817|SGH-A827|SGH-A837|SGH-A847|SGH-A867|SGH-A877|SGH-A887|SGH-A897|SGH-A927|SGH-B100|SGH-B130|SGH-B200|SGH-B220|SGH-C100|SGH-C110|SGH-C120|SGH-C130|SGH-C140|SGH-C160|SGH-C170|SGH-C180|SGH-C200|SGH-C207|SGH-C210|SGH-C225|SGH-C230|SGH-C417|SGH-C450|SGH-D307|SGH-D347|SGH-D357|SGH-D407|SGH-D415|SGH-D780|SGH-D807|SGH-D980|SGH-E105|SGH-E200|SGH-E315|SGH-E316|SGH-E317|SGH-E335|SGH-E590|SGH-E635|SGH-E715|SGH-E890|SGH-F300|SGH-F480|SGH-I200|SGH-I300|SGH-I320|SGH-I550|SGH-I577|SGH-I600|SGH-I607|SGH-I617|SGH-I627|SGH-I637|SGH-I677|SGH-I700|SGH-I717|SGH-I727|SGH-i747M|SGH-I777|SGH-I780|SGH-I827|SGH-I847|SGH-I857|SGH-I896|SGH-I897|SGH-I900|SGH-I907|SGH-I917|SGH-I927|SGH-I937|SGH-I997|SGH-J150|SGH-J200|SGH-L170|SGH-L700|SGH-M110|SGH-M150|SGH-M200|SGH-N105|SGH-N500|SGH-N600|SGH-N620|SGH-N625|SGH-N700|SGH-N710|SGH-P107|SGH-P207|SGH-P300|SGH-P310|SGH-P520|SGH-P735|SGH-P777|SGH-Q105|SGH-R210|SGH-R220|SGH-R225|SGH-S105|SGH-S307|SGH-T109|SGH-T119|SGH-T139|SGH-T209|SGH-T219|SGH-T229|SGH-T239|SGH-T249|SGH-T259|SGH-T309|SGH-T319|SGH-T329|SGH-T339|SGH-T349|SGH-T359|SGH-T369|SGH-T379|SGH-T409|SGH-T429|SGH-T439|SGH-T459|SGH-T469|SGH-T479|SGH-T499|SGH-T509|SGH-T519|SGH-T539|SGH-T559|SGH-T589|SGH-T609|SGH-T619|SGH-T629|SGH-T639|SGH-T659|SGH-T669|SGH-T679|SGH-T709|SGH-T719|SGH-T729|SGH-T739|SGH-T746|SGH-T749|SGH-T759|SGH-T769|SGH-T809|SGH-T819|SGH-T839|SGH-T919|SGH-T929|SGH-T939|SGH-T959|SGH-T989|SGH-U100|SGH-U200|SGH-U800|SGH-V205|SGH-V206|SGH-X100|SGH-X105|SGH-X120|SGH-X140|SGH-X426|SGH-X427|SGH-X475|SGH-X495|SGH-X497|SGH-X507|SGH-X600|SGH-X610|SGH-X620|SGH-X630|SGH-X700|SGH-X820|SGH-X890|SGH-Z130|SGH-Z150|SGH-Z170|SGH-ZX10|SGH-ZX20|SHW-M110|SPH-A120|SPH-A400|SPH-A420|SPH-A460|SPH-A500|SPH-A560|SPH-A600|SPH-A620|SPH-A660|SPH-A700|SPH-A740|SPH-A760|SPH-A790|SPH-A800|SPH-A820|SPH-A840|SPH-A880|SPH-A900|SPH-A940|SPH-A960|SPH-D600|SPH-D700|SPH-D710|SPH-D720|SPH-I300|SPH-I325|SPH-I330|SPH-I350|SPH-I500|SPH-I600|SPH-I700|SPH-L700|SPH-M100|SPH-M220|SPH-M240|SPH-M300|SPH-M305|SPH-M320|SPH-M330|SPH-M350|SPH-M360|SPH-M370|SPH-M380|SPH-M510|SPH-M540|SPH-M550|SPH-M560|SPH-M570|SPH-M580|SPH-M610|SPH-M620|SPH-M630|SPH-M800|SPH-M810|SPH-M850|SPH-M900|SPH-M910|SPH-M920|SPH-M930|SPH-N100|SPH-N200|SPH-N240|SPH-N300|SPH-N400|SPH-Z400|SWC-E100|SCH-i909|GT-N7100|GT-N7105|SCH-I535|SM-N900A|SGH-I317|SGH-T999L|GT-S5360B|GT-I8262|GT-S6802|GT-S6312|GT-S6310|GT-S5312|GT-S5310|GT-I9105|GT-I8510|GT-S6790N|SM-G7105|SM-N9005|GT-S5301|GT-I9295|GT-I9195|SM-C101|GT-S7392|GT-S7560|GT-B7610|GT-I5510|GT-S7582|GT-S7530E|GT-I8750|SM-G9006V|SM-G9008V|SM-G9009D|SM-G900A|SM-G900D|SM-G900F|SM-G900H|SM-G900I|SM-G900J|SM-G900K|SM-G900L|SM-G900M|SM-G900P|SM-G900R4|SM-G900S|SM-G900T|SM-G900V|SM-G900W8|SHV-E160K|SCH-P709|SCH-P729|SM-T2558|GT-I9205|SM-G9350|SM-J120F|SM-G920F|SM-G920V|SM-G930F|SM-N910C|SM-A310F|GT-I9190|SM-J500FN|SM-G903F|SM-J330F|SM-G610F|SM-G981B|SM-G892A|SM-A530F|SM-G988N|SM-G781B|SM-A805N|SM-G965F', 'LG' => '\bLG\b;|LG[- ]?(C800|C900|E400|E610|E900|E-900|F160|F180K|F180L|F180S|730|855|L160|LS740|LS840|LS970|LU6200|MS690|MS695|MS770|MS840|MS870|MS910|P500|P700|P705|VM696|AS680|AS695|AX840|C729|E970|GS505|272|C395|E739BK|E960|L55C|L75C|LS696|LS860|P769BK|P350|P500|P509|P870|UN272|US730|VS840|VS950|LN272|LN510|LS670|LS855|LW690|MN270|MN510|P509|P769|P930|UN200|UN270|UN510|UN610|US670|US740|US760|UX265|UX840|VN271|VN530|VS660|VS700|VS740|VS750|VS910|VS920|VS930|VX9200|VX11000|AX840A|LW770|P506|P925|P999|E612|D955|D802|MS323|M257)|LM-G710', 'Sony' => 'SonyST|SonyLT|SonyEricsson|SonyEricssonLT15iv|LT18i|E10i|LT28h|LT26w|SonyEricssonMT27i|C5303|C6902|C6903|C6906|C6943|D2533|SOV34|601SO|F8332', 'Asus' => 'Asus.*Galaxy|PadFone.*Mobile|ASUS_Z01QD|ASUS_X00TD', 'Xiaomi' => '^(?!.*\bx11\b).*xiaomi.*$|POCOPHONE F1|\bMI\b 8|\bMi\b 10|Redmi Note 9S|Redmi 5A|Redmi Note 5A Prime|Redmi Note 7 Pro|N2G47H|M2001J2G|M2001J2I|M1805E10A|M2004J11G|M1902F1G|M2002J9G|M2004J19G|M2003J6A1G|M2012K11C|M2007J1SC', 'NokiaLumia' => 'Lumia [0-9]{3,4}', // http://www.micromaxinfo.com/mobiles/smartphones // Added because the codes might conflict with Acer Tablets. 'Micromax' => 'Micromax.*\b(A210|A92|A88|A72|A111|A110Q|A115|A116|A110|A90S|A26|A51|A35|A54|A25|A27|A89|A68|A65|A57|A90)\b', // @todo Complete the regex. 'Palm' => 'PalmSource|Palm', // avantgo|blazer|elaine|hiptop|plucker|xiino ; 'Vertu' => 'Vertu|Vertu.*Ltd|Vertu.*Ascent|Vertu.*Ayxta|Vertu.*Constellation(F|Quest)?|Vertu.*Monika|Vertu.*Signature', // Just for fun ;) // http://www.pantech.co.kr/en/prod/prodList.do?gbrand=VEGA (PANTECH) // Most of the VEGA devices are legacy. PANTECH seem to be newer devices based on Android. 'Pantech' => 'PANTECH|IM-A850S|IM-A840S|IM-A830L|IM-A830K|IM-A830S|IM-A820L|IM-A810K|IM-A810S|IM-A800S|IM-T100K|IM-A725L|IM-A780L|IM-A775C|IM-A770K|IM-A760S|IM-A750K|IM-A740S|IM-A730S|IM-A720L|IM-A710K|IM-A690L|IM-A690S|IM-A650S|IM-A630K|IM-A600S|VEGA PTL21|PT003|P8010|ADR910L|P6030|P6020|P9070|P4100|P9060|P5000|CDM8992|TXT8045|ADR8995|IS11PT|P2030|P6010|P8000|PT002|IS06|CDM8999|P9050|PT001|TXT8040|P2020|P9020|P2000|P7040|P7000|C790', // http://www.fly-phone.com/devices/smartphones/ ; Included only smartphones. 'Fly' => 'IQ230|IQ444|IQ450|IQ440|IQ442|IQ441|IQ245|IQ256|IQ236|IQ255|IQ235|IQ245|IQ275|IQ240|IQ285|IQ280|IQ270|IQ260|IQ250', // http://fr.wikomobile.com 'Wiko' => 'KITE 4G|HIGHWAY|GETAWAY|STAIRWAY|DARKSIDE|DARKFULL|DARKNIGHT|DARKMOON|SLIDE|WAX 4G|RAINBOW|BLOOM|SUNSET|GOA(?!nna)|LENNY|BARRY|IGGY|OZZY|CINK FIVE|CINK PEAX|CINK PEAX 2|CINK SLIM|CINK SLIM 2|CINK +|CINK KING|CINK PEAX|CINK SLIM|SUBLIM', 'iMobile' => 'i-mobile (IQ|i-STYLE|idea|ZAA|Hitz)', // Added simvalley mobile just for fun. They have some interesting devices. // http://www.simvalley.fr/telephonie---gps-_22_telephonie-mobile_telephones_.html 'SimValley' => '\b(SP-80|XT-930|SX-340|XT-930|SX-310|SP-360|SP60|SPT-800|SP-120|SPT-800|SP-140|SPX-5|SPX-8|SP-100|SPX-8|SPX-12)\b', // Wolfgang - a brand that is sold by Aldi supermarkets. // http://www.wolfgangmobile.com/ 'Wolfgang' => 'AT-B24D|AT-AS50HD|AT-AS40W|AT-AS55HD|AT-AS45q2|AT-B26D|AT-AS50Q', 'Alcatel' => 'Alcatel', 'Nintendo' => 'Nintendo (3DS|Switch)', // http://en.wikipedia.org/wiki/Amoi 'Amoi' => 'Amoi', // http://en.wikipedia.org/wiki/INQ 'INQ' => 'INQ', 'OnePlus' => 'ONEPLUS', // @Tapatalk is a mobile app; http://support.tapatalk.com/threads/smf-2-0-2-os-and-browser-detection-plugin-and-tapatalk.15565/#post-79039 'GenericPhone' => 'Tapatalk|PDA;|SAGEM|\bmmp\b|pocket|\bpsp\b|symbian|Smartphone|smartfon|treo|up.browser|up.link|vodafone|\bwap\b|nokia|Series40|Series60|S60|SonyEricsson|N900|MAUI.*WAP.*Browser', ]; /** * List of tablet devices. * * @var array */ protected static array $tabletDevices = [ // @todo: check for mobile friendly emails topic. 'iPad' => 'iPad|iPad.*Mobile', // Removed |^.*Android.*Nexus(?!(?:Mobile).)*$ // @see #442 // @todo Merge NexusTablet into GoogleTablet. 'NexusTablet' => 'Android.*Nexus[\s]+(7|9|10)', // https://en.wikipedia.org/wiki/Pixel_C 'GoogleTablet' => 'Android.*Pixel C', 'SamsungTablet' => 'SAMSUNG.*Tablet|Galaxy.*Tab|SC-01C|GT-P1000|GT-P1003|GT-P1010|GT-P3105|GT-P6210|GT-P6800|GT-P6810|GT-P7100|GT-P7300|GT-P7310|GT-P7500|GT-P7510|SCH-I800|SCH-I815|SCH-I905|SGH-I957|SGH-I987|SGH-T849|SGH-T859|SGH-T869|SPH-P100|GT-P3100|GT-P3108|GT-P3110|GT-P5100|GT-P5110|GT-P6200|GT-P7320|GT-P7511|GT-N8000|GT-P8510|SGH-I497|SPH-P500|SGH-T779|SCH-I705|SCH-I915|GT-N8013|GT-P3113|GT-P5113|GT-P8110|GT-N8010|GT-N8005|GT-N8020|GT-P1013|GT-P6201|GT-P7501|GT-N5100|GT-N5105|GT-N5110|SHV-E140K|SHV-E140L|SHV-E140S|SHV-E150S|SHV-E230K|SHV-E230L|SHV-E230S|SHW-M180K|SHW-M180L|SHW-M180S|SHW-M180W|SHW-M300W|SHW-M305W|SHW-M380K|SHW-M380S|SHW-M380W|SHW-M430W|SHW-M480K|SHW-M480S|SHW-M480W|SHW-M485W|SHW-M486W|SHW-M500W|GT-I9228|SCH-P739|SCH-I925|GT-I9200|GT-P5200|GT-P5210|GT-P5210X|SM-T311|SM-T310|SM-T310X|SM-T210|SM-T210R|SM-T211|SM-P600|SM-P601|SM-P605|SM-P900|SM-P901|SM-T217|SM-T217A|SM-T217S|SM-P6000|SM-T3100|SGH-I467|XE500|SM-T110|GT-P5220|GT-I9200X|GT-N5110X|GT-N5120|SM-P905|SM-T111|SM-T2105|SM-T315|SM-T320|SM-T320X|SM-T321|SM-T520|SM-T525|SM-T530NU|SM-T230NU|SM-T330NU|SM-T900|XE500T1C|SM-P605V|SM-P905V|SM-T337V|SM-T537V|SM-T707V|SM-T807V|SM-P600X|SM-P900X|SM-T210X|SM-T230|SM-T230X|SM-T325|GT-P7503|SM-T531|SM-T330|SM-T530|SM-T705|SM-T705C|SM-T535|SM-T331|SM-T800|SM-T700|SM-T537|SM-T807|SM-P907A|SM-T337A|SM-T537A|SM-T707A|SM-T807A|SM-T237|SM-T807P|SM-P607T|SM-T217T|SM-T337T|SM-T807T|SM-T116NQ|SM-T116BU|SM-P550|SM-T350|SM-T550|SM-T9000|SM-P9000|SM-T705Y|SM-T805|GT-P3113|SM-T710|SM-T810|SM-T815|SM-T360|SM-T533|SM-T113|SM-T335|SM-T715|SM-T560|SM-T670|SM-T677|SM-T377|SM-T567|SM-T357T|SM-T555|SM-T561|SM-T713|SM-T719|SM-T813|SM-T819|SM-T580|SM-T355Y?|SM-T280|SM-T817A|SM-T820|SM-W700|SM-P580|SM-T587|SM-P350|SM-P555M|SM-P355M|SM-T113NU|SM-T815Y|SM-T585|SM-T285|SM-T825|SM-W708|SM-T835|SM-T830|SM-T837V|SM-T720|SM-T510|SM-T387V|SM-P610|SM-T290|SM-T515|SM-T590|SM-T595|SM-T725|SM-T817P|SM-P585N0|SM-T395|SM-T295|SM-T865|SM-P610N|SM-P615|SM-T970|SM-T380|SM-T5950|SM-T905|SM-T231|SM-T500|SM-T860|SM-T536|SM-T837A|SM-X200|SM-T220|SM-T870|SM-X906C|SM-X700|SM-X706|SM-X706B|SM-X706U|SM-X706N|SM-X800|SM-X806|SM-X806B|SM-X806U|SM-X806N|SM-X900|SM-X906|SM-X906B|SM-X906U|SM-X906N|SM-P613', // SCH-P709|SCH-P729|SM-T2558|GT-I9205 - Samsung Mega - treat them like a regular phone. // http://docs.aws.amazon.com/silk/latest/developerguide/user-agent.html 'Kindle' => 'Kindle|Silk.*Accelerated|Android.*\b(KFOT|KFTT|KFJWI|KFJWA|KFOTE|KFSOWI|KFTHWI|KFTHWA|KFAPWI|KFAPWA|WFJWAE|KFSAWA|KFSAWI|KFASWI|KFARWI|KFFOWI|KFGIWI|KFMEWI)\b|Android.*Silk/[0-9.]+ like Chrome/[0-9.]+ (?!Mobile)', // Only the Surface tablets with Windows RT are considered mobile. // http://msdn.microsoft.com/en-us/library/ie/hh920767(v=vs.85).aspx 'SurfaceTablet' => 'Windows NT [0-9.]+; ARM;.*(Tablet|ARMBJS)', // http://shopping1.hp.com/is-bin/INTERSHOP.enfinity/WFS/WW-USSMBPublicStore-Site/en_US/-/USD/ViewStandardCatalog-Browse?CatalogCategoryID=JfIQ7EN5lqMAAAEyDcJUDwMT 'HPTablet' => 'HP Slate (7|8|10)|HP ElitePad 900|hp-tablet|EliteBook.*Touch|HP 8|Slate 21|HP SlateBook 10', // Watch out for PadFone, see #132. // http://www.asus.com/de/Tablets_Mobile/Memo_Pad_Products/ 'AsusTablet' => '^.*PadFone((?!Mobile).)*$|Transformer|TF101|TF101G|TF300T|TF300TG|TF300TL|TF700T|TF700KL|TF701T|TF810C|ME171|ME301T|ME302C|ME371MG|ME370T|ME372MG|ME172V|ME173X|ME400C|Slider SL101|\bK00F\b|\bK00C\b|\bK00E\b|\bK00L\b|TX201LA|ME176C|ME102A|\bM80TA\b|ME372CL|ME560CG|ME372CG|ME302KL| K01A | K010 | K011 | K017 | K01E |ME572C|ME103K|ME170C|ME171C|\bME70C\b|ME581C|ME581CL|ME8510C|ME181C|P01Y|PO1MA|P01Z|\bP027\b|\bP024\b|\bP00C\b', 'BlackBerryTablet' => 'PlayBook|RIM Tablet', 'HTCtablet' => 'HTC_Flyer_P512|HTC Flyer|HTC Jetstream|HTC-P715a|HTC EVO View 4G|PG41200|PG09410', 'MotorolaTablet' => 'xoom|sholest|MZ615|MZ605|MZ505|MZ601|MZ602|MZ603|MZ604|MZ606|MZ607|MZ608|MZ609|MZ615|MZ616|MZ617', 'NookTablet' => 'Android.*Nook|NookColor|nook browser|BNRV200|BNRV200A|BNTV250|BNTV250A|BNTV400|BNTV600|LogicPD Zoom2', // http://www.acer.ro/ac/ro/RO/content/drivers // http://www.packardbell.co.uk/pb/en/GB/content/download (Packard Bell is part of Acer) // http://us.acer.com/ac/en/US/content/group/tablets // http://www.acer.de/ac/de/DE/content/models/tablets/ // Can conflict with Micromax and Motorola phones codes. 'AcerTablet' => 'Android.*; \b(A100|A101|A110|A200|A210|A211|A500|A501|A510|A511|A700|A701|W500|W500P|W501|W501P|W510|W511|W700|G100|G100W|B1-A71|B1-710|B1-711|A1-810|A1-811|A1-830)\b|W3-810|\bA3-A10\b|\bA3-A11\b|\bA3-A20\b|\bA3-A30|A3-A40', // http://eu.computers.toshiba-europe.com/innovation/family/Tablets/1098744/banner_id/tablet_footerlink/ // http://us.toshiba.com/tablets/tablet-finder // http://www.toshiba.co.jp/regza/tablet/ 'ToshibaTablet' => 'Android.*(AT100|AT105|AT200|AT205|AT270|AT275|AT300|AT305|AT1S5|AT500|AT570|AT700|AT830)|TOSHIBA.*FOLIO', // http://www.nttdocomo.co.jp/english/service/developer/smart_phone/technical_info/spec/index.html // http://www.lg.com/us/tablets 'LGTablet' => '\bL-06C|LG-V909|LG-V900|LG-V700|LG-V510|LG-V500|LG-V410|LG-V400|LG-VK810\b', 'FujitsuTablet' => 'Android.*\b(F-01D|F-02F|F-05E|F-10D|M532|Q572)\b', // Prestigio Tablets http://www.prestigio.com/support 'PrestigioTablet' => 'PMP3170B|PMP3270B|PMP3470B|PMP7170B|PMP3370B|PMP3570C|PMP5870C|PMP3670B|PMP5570C|PMP5770D|PMP3970B|PMP3870C|PMP5580C|PMP5880D|PMP5780D|PMP5588C|PMP7280C|PMP7280C3G|PMP7280|PMP7880D|PMP5597D|PMP5597|PMP7100D|PER3464|PER3274|PER3574|PER3884|PER5274|PER5474|PMP5097CPRO|PMP5097|PMP7380D|PMP5297C|PMP5297C_QUAD|PMP812E|PMP812E3G|PMP812F|PMP810E|PMP880TD|PMT3017|PMT3037|PMT3047|PMT3057|PMT7008|PMT5887|PMT5001|PMT5002', // http://support.lenovo.com/en_GB/downloads/default.page?# 'LenovoTablet' => 'Lenovo TAB|Idea(Tab|Pad)( A1|A10| K1|)|ThinkPad([ ]+)?Tablet|YT3-850M|YT3-X90L|YT3-X90F|YT3-X90X|Lenovo.*(S2109|S2110|S5000|S6000|K3011|A3000|A3500|A1000|A2107|A2109|A1107|A5500|A7600|B6000|B8000|B8080)(-|)(FL|F|HV|H|)|TB-X103F|TB-X304X|TB-X304F|TB-X304L|TB-X505F|TB-X505L|TB-X505X|TB-X605F|TB-X605L|TB-8703F|TB-8703X|TB-8703N|TB-8704N|TB-8704F|TB-8704X|TB-8704V|TB-7304F|TB-7304I|TB-7304X|Tab2A7-10F|Tab2A7-20F|TB2-X30L|YT3-X50L|YT3-X50F|YT3-X50M|YT-X705F|YT-X703F|YT-X703L|YT-X705L|YT-X705X|TB2-X30F|TB2-X30L|TB2-X30M|A2107A-F|A2107A-H|TB3-730F|TB3-730M|TB3-730X|TB-7504F|TB-7504X|TB-X704F|TB-X104F|TB3-X70F|TB-X705F|TB-8504F|TB3-X70L|TB3-710F|TB-X704L|TB-J606F|TB-X606F|TB-X306X|YT-J706X|TB128FU', // http://www.dell.com/support/home/us/en/04/Products/tab_mob/tablets 'DellTablet' => 'Venue 11|Venue 8|Venue 7|Dell Streak 10|Dell Streak 7', 'XiaomiTablet' => '21051182G', // http://www.yarvik.com/en/matrix/tablets/ 'YarvikTablet' => 'Android.*\b(TAB210|TAB211|TAB224|TAB250|TAB260|TAB264|TAB310|TAB360|TAB364|TAB410|TAB411|TAB420|TAB424|TAB450|TAB460|TAB461|TAB464|TAB465|TAB467|TAB468|TAB07-100|TAB07-101|TAB07-150|TAB07-151|TAB07-152|TAB07-200|TAB07-201-3G|TAB07-210|TAB07-211|TAB07-212|TAB07-214|TAB07-220|TAB07-400|TAB07-485|TAB08-150|TAB08-200|TAB08-201-3G|TAB08-201-30|TAB09-100|TAB09-211|TAB09-410|TAB10-150|TAB10-201|TAB10-211|TAB10-400|TAB10-410|TAB13-201|TAB274EUK|TAB275EUK|TAB374EUK|TAB462EUK|TAB474EUK|TAB9-200)\b', 'MedionTablet' => 'Android.*\bOYO\b|LIFE.*(P9212|P9514|P9516|S9512)|LIFETAB', 'ArnovaTablet' => '97G4|AN10G2|AN7bG3|AN7fG3|AN8G3|AN8cG3|AN7G3|AN9G3|AN7dG3|AN7dG3ST|AN7dG3ChildPad|AN10bG3|AN10bG3DT|AN9G2', // http://www.intenso.de/kategorie_en.php?kategorie=33 // @todo: http://www.nbhkdz.com/read/b8e64202f92a2df129126bff.html - investigate 'IntensoTablet' => 'INM8002KP|INM1010FP|INM805ND|Intenso Tab|TAB1004', // IRU.ru Tablets http://www.iru.ru/catalog/soho/planetable/ 'IRUTablet' => 'M702pro', 'MegafonTablet' => 'MegaFon V9|\bZTE V9\b|Android.*\bMT7A\b', // http://www.e-boda.ro/tablete-pc.html 'EbodaTablet' => 'E-Boda (Supreme|Impresspeed|Izzycomm|Essential)', // http://www.allview.ro/produse/droseries/lista-tablete-pc/ 'AllViewTablet' => 'Allview.*(Viva|Alldro|City|Speed|All TV|Frenzy|Quasar|Shine|TX1|AX1|AX2)', // http://wiki.archosfans.com/index.php?title=Main_Page // @note Rewrite the regex format after we add more UAs. 'ArchosTablet' => '\b(101G9|80G9|A101IT)\b|Qilive 97R|Archos5|\bARCHOS (70|79|80|90|97|101|FAMILYPAD|)(b|c|)(G10| Cobalt| TITANIUM(HD|)| Xenon| Neon|XSK| 2| XS 2| PLATINUM| CARBON|GAMEPAD)\b', // http://www.ainol.com/plugin.php?identifier=ainol&module=product 'AinolTablet' => 'NOVO7|NOVO8|NOVO10|Novo7Aurora|Novo7Basic|NOVO7PALADIN|novo9-Spark', 'NokiaLumiaTablet' => 'Lumia 2520', // @todo: inspect http://esupport.sony.com/US/p/select-system.pl?DIRECTOR=DRIVER // Readers http://www.atsuhiro-me.net/ebook/sony-reader/sony-reader-web-browser // http://www.sony.jp/support/tablet/ 'SonyTablet' => 'Sony.*Tablet|Xperia Tablet|Sony Tablet S|SO-03E|SGPT12|SGPT13|SGPT114|SGPT121|SGPT122|SGPT123|SGPT111|SGPT112|SGPT113|SGPT131|SGPT132|SGPT133|SGPT211|SGPT212|SGPT213|SGP311|SGP312|SGP321|EBRD1101|EBRD1102|EBRD1201|SGP351|SGP341|SGP511|SGP512|SGP521|SGP541|SGP551|SGP621|SGP641|SGP612|SOT31|SGP771|SGP611|SGP612|SGP712', // http://www.support.philips.com/support/catalog/worldproducts.jsp?userLanguage=en&userCountry=cn&categoryid=3G_LTE_TABLET_SU_CN_CARE&title=3G%20tablets%20/%20LTE%20range&_dyncharset=UTF-8 'PhilipsTablet' => '\b(PI2010|PI3000|PI3100|PI3105|PI3110|PI3205|PI3210|PI3900|PI4010|PI7000|PI7100)\b', // db + http://www.cube-tablet.com/buy-products.html 'CubeTablet' => 'Android.*(K8GT|U9GT|U10GT|U16GT|U17GT|U18GT|U19GT|U20GT|U23GT|U30GT)|CUBE U8GT', // http://www.cobyusa.com/?p=pcat&pcat_id=3001 'CobyTablet' => 'MID1042|MID1045|MID1125|MID1126|MID7012|MID7014|MID7015|MID7034|MID7035|MID7036|MID7042|MID7048|MID7127|MID8042|MID8048|MID8127|MID9042|MID9740|MID9742|MID7022|MID7010', // http://www.match.net.cn/products.asp 'MIDTablet' => 'M9701|M9000|M9100|M806|M1052|M806|T703|MID701|MID713|MID710|MID727|MID760|MID830|MID728|MID933|MID125|MID810|MID732|MID120|MID930|MID800|MID731|MID900|MID100|MID820|MID735|MID980|MID130|MID833|MID737|MID960|MID135|MID860|MID736|MID140|MID930|MID835|MID733|MID4X10', // http://www.msi.com/support // @todo Research the Windows Tablets. 'MSITablet' => 'MSI \b(Primo 73K|Primo 73L|Primo 81L|Primo 77|Primo 93|Primo 75|Primo 76|Primo 73|Primo 81|Primo 91|Primo 90|Enjoy 71|Enjoy 7|Enjoy 10)\b', // @todo http://www.kyoceramobile.com/support/drivers/ // 'KyoceraTablet' => null, // @todo http://intexuae.com/index.php/category/mobile-devices/tablets-products/ // 'IntextTablet' => null, // http://pdadb.net/index.php?m=pdalist&list=SMiT (NoName Chinese Tablets) // http://www.imp3.net/14/show.php?itemid=20454 'SMiTTablet' => 'Android.*(\bMID\b|MID-560|MTV-T1200|MTV-PND531|MTV-P1101|MTV-PND530)', // http://www.rock-chips.com/index.php?do=prod&pid=2 'RockChipTablet' => 'Android.*(RK2818|RK2808A|RK2918|RK3066)|RK2738|RK2808A', // http://www.fly-phone.com/devices/tablets/ ; http://www.fly-phone.com/service/ 'FlyTablet' => 'IQ310|Fly Vision', // http://www.bqreaders.com/gb/tablets-prices-sale.html 'bqTablet' => 'Android.*(bq)?.*\b(Elcano|Curie|Edison|Maxwell|Kepler|Pascal|Tesla|Hypatia|Platon|Newton|Livingstone|Cervantes|Avant|Aquaris ([E|M]10|M8))\b|Maxwell.*Lite|Maxwell.*Plus', // http://www.huaweidevice.com/worldwide/productFamily.do?method=index&directoryId=5011&treeId=3290 // http://www.huaweidevice.com/worldwide/downloadCenter.do?method=index&directoryId=3372&treeId=0&tb=1&type=software (including legacy tablets) 'HuaweiTablet' => 'MediaPad|MediaPad 7 Youth|IDEOS S7|S7-201c|S7-202u|S7-101|S7-103|S7-104|S7-105|S7-106|S7-201|S7-Slim|M2-A01L|BAH-L09|BAH-W09|AGS-L09|CMR-AL19|KOB2-L09|BG2-U01|BG2-W09|BG2-U03', // Nec or Medias Tab 'NecTablet' => '\bN-06D|\bN-08D', // Pantech Tablets: http://www.pantechusa.com/phones/ 'PantechTablet' => 'Pantech.*P4100', // Broncho Tablets: http://www.broncho.cn/ (hard to find) 'BronchoTablet' => 'Broncho.*(N701|N708|N802|a710)', // http://versusuk.com/support.html 'VersusTablet' => 'TOUCHPAD.*[78910]|\bTOUCHTAB\b', // http://www.zync.in/index.php/our-products/tablet-phablets 'ZyncTablet' => 'z1000|Z99 2G|z930|z990|z909|Z919|z900', // Removed "z999" because of https://github.com/serbanghita/Mobile-Detect/issues/717 // http://www.positivoinformatica.com.br/www/pessoal/tablet-ypy/ 'PositivoTablet' => 'TB07STA|TB10STA|TB07FTA|TB10FTA', // https://www.nabitablet.com/ 'NabiTablet' => 'Android.*\bNabi', 'KoboTablet' => 'Kobo Touch|\bK080\b|\bVox\b Build|\bArc\b Build', // French Danew Tablets http://www.danew.com/produits-tablette.php 'DanewTablet' => 'DSlide.*\b(700|701R|702|703R|704|802|970|971|972|973|974|1010|1012)\b', // Texet Tablets and Readers http://www.texet.ru/tablet/ 'TexetTablet' => 'NaviPad|TB-772A|TM-7045|TM-7055|TM-9750|TM-7016|TM-7024|TM-7026|TM-7041|TM-7043|TM-7047|TM-8041|TM-9741|TM-9747|TM-9748|TM-9751|TM-7022|TM-7021|TM-7020|TM-7011|TM-7010|TM-7023|TM-7025|TM-7037W|TM-7038W|TM-7027W|TM-9720|TM-9725|TM-9737W|TM-1020|TM-9738W|TM-9740|TM-9743W|TB-807A|TB-771A|TB-727A|TB-725A|TB-719A|TB-823A|TB-805A|TB-723A|TB-715A|TB-707A|TB-705A|TB-709A|TB-711A|TB-890HD|TB-880HD|TB-790HD|TB-780HD|TB-770HD|TB-721HD|TB-710HD|TB-434HD|TB-860HD|TB-840HD|TB-760HD|TB-750HD|TB-740HD|TB-730HD|TB-722HD|TB-720HD|TB-700HD|TB-500HD|TB-470HD|TB-431HD|TB-430HD|TB-506|TB-504|TB-446|TB-436|TB-416|TB-146SE|TB-126SE', // Avoid detecting 'PLAYSTATION 3' as mobile. 'PlaystationTablet' => 'Playstation.*(Portable|Vita)', // http://www.trekstor.de/surftabs.html 'TrekstorTablet' => 'ST10416-1|VT10416-1|ST70408-1|ST702xx-1|ST702xx-2|ST80208|ST97216|ST70104-2|VT10416-2|ST10216-2A|SurfTab', // http://www.pyleaudio.com/Products.aspx?%2fproducts%2fPersonal-Electronics%2fTablets 'PyleAudioTablet' => '\b(PTBL10CEU|PTBL10C|PTBL72BC|PTBL72BCEU|PTBL7CEU|PTBL7C|PTBL92BC|PTBL92BCEU|PTBL9CEU|PTBL9CUK|PTBL9C)\b', // http://www.advandigital.com/index.php?link=content-product&jns=JP001 // because of the short codenames we have to include whitespaces to reduce the possible conflicts. 'AdvanTablet' => 'Android.* \b(E3A|T3X|T5C|T5B|T3E|T3C|T3B|T1J|T1F|T2A|T1H|T1i|E1C|T1-E|T5-A|T4|E1-B|T2Ci|T1-B|T1-D|O1-A|E1-A|T1-A|T3A|T4i)\b ', // http://www.danytech.com/category/tablet-pc 'DanyTechTablet' => 'Genius Tab G3|Genius Tab S2|Genius Tab Q3|Genius Tab G4|Genius Tab Q4|Genius Tab G-II|Genius TAB GII|Genius TAB GIII|Genius Tab S1', // http://www.galapad.net/product.html ; https://github.com/serbanghita/Mobile-Detect/issues/761 'GalapadTablet' => 'Android [0-9.]+; [a-z-]+; \bG1\b', // http://www.micromaxinfo.com/tablet/funbook 'MicromaxTablet' => 'Funbook|Micromax.*\b(P250|P560|P360|P362|P600|P300|P350|P500|P275)\b', // http://www.karbonnmobiles.com/products_tablet.php 'KarbonnTablet' => 'Android.*\b(A39|A37|A34|ST8|ST10|ST7|Smart Tab3|Smart Tab2)\b', // http://www.myallfine.com/Products.asp 'AllFineTablet' => 'Fine7 Genius|Fine7 Shine|Fine7 Air|Fine8 Style|Fine9 More|Fine10 Joy|Fine11 Wide', // http://www.proscanvideo.com/products-search.asp?itemClass=TABLET&itemnmbr= 'PROSCANTablet' => '\b(PEM63|PLT1023G|PLT1041|PLT1044|PLT1044G|PLT1091|PLT4311|PLT4311PL|PLT4315|PLT7030|PLT7033|PLT7033D|PLT7035|PLT7035D|PLT7044K|PLT7045K|PLT7045KB|PLT7071KG|PLT7072|PLT7223G|PLT7225G|PLT7777G|PLT7810K|PLT7849G|PLT7851G|PLT7852G|PLT8015|PLT8031|PLT8034|PLT8036|PLT8080K|PLT8082|PLT8088|PLT8223G|PLT8234G|PLT8235G|PLT8816K|PLT9011|PLT9045K|PLT9233G|PLT9735|PLT9760G|PLT9770G)\b', // http://www.yonesnav.com/products/products.php 'YONESTablet' => 'BQ1078|BC1003|BC1077|RK9702|BC9730|BC9001|IT9001|BC7008|BC7010|BC708|BC728|BC7012|BC7030|BC7027|BC7026', // http://www.cjshowroom.com/eproducts.aspx?classcode=004001001 // China manufacturer makes tablets for different small brands (eg. http://www.zeepad.net/index.html) 'ChangJiaTablet' => 'TPC7102|TPC7103|TPC7105|TPC7106|TPC7107|TPC7201|TPC7203|TPC7205|TPC7210|TPC7708|TPC7709|TPC7712|TPC7110|TPC8101|TPC8103|TPC8105|TPC8106|TPC8203|TPC8205|TPC8503|TPC9106|TPC9701|TPC97101|TPC97103|TPC97105|TPC97106|TPC97111|TPC97113|TPC97203|TPC97603|TPC97809|TPC97205|TPC10101|TPC10103|TPC10106|TPC10111|TPC10203|TPC10205|TPC10503', // http://www.gloryunion.cn/products.asp // http://www.allwinnertech.com/en/apply/mobile.html // http://www.ptcl.com.pk/pd_content.php?pd_id=284 (EVOTAB) // @todo: Softwiner tablets? // aka. Cute or Cool tablets. Not sure yet, must research to avoid collisions. 'GUTablet' => 'TX-A1301|TX-M9002|Q702|kf026', // A12R|D75A|D77|D79|R83|A95|A106C|R15|A75|A76|D71|D72|R71|R73|R77|D82|R85|D92|A97|D92|R91|A10F|A77F|W71F|A78F|W78F|W81F|A97F|W91F|W97F|R16G|C72|C73E|K72|K73|R96G // http://www.pointofview-online.com/showroom.php?shop_mode=product_listing&category_id=118 'PointOfViewTablet' => 'TAB-P506|TAB-navi-7-3G-M|TAB-P517|TAB-P-527|TAB-P701|TAB-P703|TAB-P721|TAB-P731N|TAB-P741|TAB-P825|TAB-P905|TAB-P925|TAB-PR945|TAB-PL1015|TAB-P1025|TAB-PI1045|TAB-P1325|TAB-PROTAB[0-9]+|TAB-PROTAB25|TAB-PROTAB26|TAB-PROTAB27|TAB-PROTAB26XL|TAB-PROTAB2-IPS9|TAB-PROTAB30-IPS9|TAB-PROTAB25XXL|TAB-PROTAB26-IPS10|TAB-PROTAB30-IPS10', // http://www.overmax.pl/pl/katalog-produktow,p8/tablety,c14/ // @todo: add more tests. 'OvermaxTablet' => 'OV-(SteelCore|NewBase|Basecore|Baseone|Exellen|Quattor|EduTab|Solution|ACTION|BasicTab|TeddyTab|MagicTab|Stream|TB-08|TB-09)|Qualcore 1027', // http://hclmetablet.com/India/index.php 'HCLTablet' => 'HCL.*Tablet|Connect-3G-2.0|Connect-2G-2.0|ME Tablet U1|ME Tablet U2|ME Tablet G1|ME Tablet X1|ME Tablet Y2|ME Tablet Sync', // http://www.edigital.hu/Tablet_es_e-book_olvaso/Tablet-c18385.html 'DPSTablet' => 'DPS Dream 9|DPS Dual 7', // http://www.visture.com/index.asp 'VistureTablet' => 'V97 HD|i75 3G|Visture V4( HD)?|Visture V5( HD)?|Visture V10', // http://www.mijncresta.nl/tablet 'CrestaTablet' => 'CTP(-)?810|CTP(-)?818|CTP(-)?828|CTP(-)?838|CTP(-)?888|CTP(-)?978|CTP(-)?980|CTP(-)?987|CTP(-)?988|CTP(-)?989', // MediaTek - http://www.mediatek.com/_en/01_products/02_proSys.php?cata_sn=1&cata1_sn=1&cata2_sn=309 'MediatekTablet' => '\bMT8125|MT8389|MT8135|MT8377\b', // Concorde tab 'ConcordeTablet' => 'Concorde([ ]+)?Tab|ConCorde ReadMan', // GoClever Tablets - http://www.goclever.com/uk/products,c1/tablet,c5/ 'GoCleverTablet' => 'GOCLEVER TAB|A7GOCLEVER|M1042|M7841|M742|R1042BK|R1041|TAB A975|TAB A7842|TAB A741|TAB A741L|TAB M723G|TAB M721|TAB A1021|TAB I921|TAB R721|TAB I720|TAB T76|TAB R70|TAB R76.2|TAB R106|TAB R83.2|TAB M813G|TAB I721|GCTA722|TAB I70|TAB I71|TAB S73|TAB R73|TAB R74|TAB R93|TAB R75|TAB R76.1|TAB A73|TAB A93|TAB A93.2|TAB T72|TAB R83|TAB R974|TAB R973|TAB A101|TAB A103|TAB A104|TAB A104.2|R105BK|M713G|A972BK|TAB A971|TAB R974.2|TAB R104|TAB R83.3|TAB A1042', // Modecom Tablets - http://www.modecom.eu/tablets/portal/ 'ModecomTablet' => 'FreeTAB 9000|FreeTAB 7.4|FreeTAB 7004|FreeTAB 7800|FreeTAB 2096|FreeTAB 7.5|FreeTAB 1014|FreeTAB 1001 |FreeTAB 8001|FreeTAB 9706|FreeTAB 9702|FreeTAB 7003|FreeTAB 7002|FreeTAB 1002|FreeTAB 7801|FreeTAB 1331|FreeTAB 1004|FreeTAB 8002|FreeTAB 8014|FreeTAB 9704|FreeTAB 1003', // Vonino Tablets 'VoninoTablet' => '\b(Argus[ _]?S|Diamond[ _]?79HD|Emerald[ _]?78E|Luna[ _]?70C|Onyx[ _]?S|Onyx[ _]?Z|Orin[ _]?HD|Orin[ _]?S|Otis[ _]?S|SpeedStar[ _]?S|Magnet[ _]?M9|Primus[ _]?94[ _]?3G|Primus[ _]?94HD|Primus[ _]?QS|Android.*\bQ8\b|Sirius[ _]?EVO[ _]?QS|Sirius[ _]?QS|Spirit[ _]?S)\b', // ECS Tablets - http://www.ecs.com.tw/ECSWebSite/Product/Product_Tablet_List.aspx?CategoryID=14&MenuID=107&childid=M_107&LanID=0 'ECSTablet' => 'V07OT2|TM105A|S10OT1|TR10CS1', // Storex Tablets - http://storex.fr/espace_client/support.html // @note: no need to add all the tablet codes since they are guided by the first regex. 'StorexTablet' => 'eZee[_\']?(Tab|Go)[0-9]+|TabLC7|Looney Tunes Tab', // Generic Vodafone tablets. 'VodafoneTablet' => 'SmartTab([ ]+)?[0-9]+|SmartTabII10|SmartTabII7|VF-1497|VFD 1400', // French tablets - Essentiel B http://www.boulanger.fr/tablette_tactile_e-book/tablette_tactile_essentiel_b/cl_68908.htm?multiChoiceToDelete=brand&mc_brand=essentielb // Aka: http://www.essentielb.fr/ 'EssentielBTablet' => 'Smart[ \']?TAB[ ]+?[0-9]+|Family[ \']?TAB2', // Ross & Moor - http://ross-moor.ru/ 'RossMoorTablet' => 'RM-790|RM-997|RMD-878G|RMD-974R|RMT-705A|RMT-701|RME-601|RMT-501|RMT-711', // i-mobile http://product.i-mobilephone.com/Mobile_Device 'iMobileTablet' => 'i-mobile i-note', // http://www.tolino.de/de/vergleichen/ 'TolinoTablet' => 'tolino tab [0-9.]+|tolino shine', // AudioSonic - a Kmart brand // http://www.kmart.com.au/webapp/wcs/stores/servlet/Search?langId=-1&storeId=10701&catalogId=10001&categoryId=193001&pageSize=72¤tPage=1&searchCategory=193001%2b4294965664&sortBy=p_MaxPrice%7c1 'AudioSonicTablet' => '\bC-22Q|T7-QC|T-17B|T-17P\b', // AMPE Tablets - http://www.ampe.com.my/product-category/tablets/ // @todo: add them gradually to avoid conflicts. 'AMPETablet' => 'Android.* A78 ', // Skk Mobile - http://skkmobile.com.ph/product_tablets.php 'SkkTablet' => 'Android.* (SKYPAD|PHOENIX|CYCLOPS)', // Tecno Mobile (only tablet) - http://www.tecno-mobile.com/index.php/product?filterby=smart&list_order=all&page=1 'TecnoTablet' => 'TECNO P9|TECNO DP8D', // JXD (consoles & tablets) - http://jxd.hk/products.asp?selectclassid=009008&clsid=3 'JXDTablet' => 'Android.* \b(F3000|A3300|JXD5000|JXD3000|JXD2000|JXD300B|JXD300|S5800|S7800|S602b|S5110b|S7300|S5300|S602|S603|S5100|S5110|S601|S7100a|P3000F|P3000s|P101|P200s|P1000m|P200m|P9100|P1000s|S6600b|S908|P1000|P300|S18|S6600|S9100)\b', // i-Joy tablets - http://www.i-joy.es/en/cat/products/tablets/ 'iJoyTablet' => 'Tablet (Spirit 7|Essentia|Galatea|Fusion|Onix 7|Landa|Titan|Scooby|Deox|Stella|Themis|Argon|Unique 7|Sygnus|Hexen|Finity 7|Cream|Cream X2|Jade|Neon 7|Neron 7|Kandy|Scape|Saphyr 7|Rebel|Biox|Rebel|Rebel 8GB|Myst|Draco 7|Myst|Tab7-004|Myst|Tadeo Jones|Tablet Boing|Arrow|Draco Dual Cam|Aurix|Mint|Amity|Revolution|Finity 9|Neon 9|T9w|Amity 4GB Dual Cam|Stone 4GB|Stone 8GB|Andromeda|Silken|X2|Andromeda II|Halley|Flame|Saphyr 9,7|Touch 8|Planet|Triton|Unique 10|Hexen 10|Memphis 4GB|Memphis 8GB|Onix 10)', // http://www.intracon.eu/tablet 'FX2Tablet' => 'FX2 PAD7|FX2 PAD10', // http://www.xoro.de/produkte/ // @note: Might be the same brand with 'Simply tablets' 'XoroTablet' => 'KidsPAD 701|PAD[ ]?712|PAD[ ]?714|PAD[ ]?716|PAD[ ]?717|PAD[ ]?718|PAD[ ]?720|PAD[ ]?721|PAD[ ]?722|PAD[ ]?790|PAD[ ]?792|PAD[ ]?900|PAD[ ]?9715D|PAD[ ]?9716DR|PAD[ ]?9718DR|PAD[ ]?9719QR|PAD[ ]?9720QR|TelePAD1030|Telepad1032|TelePAD730|TelePAD731|TelePAD732|TelePAD735Q|TelePAD830|TelePAD9730|TelePAD795|MegaPAD 1331|MegaPAD 1851|MegaPAD 2151', // http://www1.viewsonic.com/products/computing/tablets/ 'ViewsonicTablet' => 'ViewPad 10pi|ViewPad 10e|ViewPad 10s|ViewPad E72|ViewPad7|ViewPad E100|ViewPad 7e|ViewSonic VB733|VB100a', // https://www.verizonwireless.com/tablets/verizon/ 'VerizonTablet' => 'QTAQZ3|QTAIR7|QTAQTZ3|QTASUN1|QTASUN2|QTAXIA1', // http://www.odys.de/web/internet-tablet_en.html 'OdysTablet' => 'LOOX|XENO10|ODYS[ -](Space|EVO|Xpress|NOON)|\bXELIO\b|Xelio10Pro|XELIO7PHONETAB|XELIO10EXTREME|XELIOPT2|NEO_QUAD10', // http://www.captiva-power.de/products.html#tablets-en 'CaptivaTablet' => 'CAPTIVA PAD', // IconBIT - http://www.iconbit.com/products/tablets/ 'IconbitTablet' => 'NetTAB|NT-3702|NT-3702S|NT-3702S|NT-3603P|NT-3603P|NT-0704S|NT-0704S|NT-3805C|NT-3805C|NT-0806C|NT-0806C|NT-0909T|NT-0909T|NT-0907S|NT-0907S|NT-0902S|NT-0902S', // http://www.teclast.com/topic.php?channelID=70&topicID=140&pid=63 'TeclastTablet' => 'T98 4G|\bP80\b|\bX90HD\b|X98 Air|X98 Air 3G|\bX89\b|P80 3G|\bX80h\b|P98 Air|\bX89HD\b|P98 3G|\bP90HD\b|P89 3G|X98 3G|\bP70h\b|P79HD 3G|G18d 3G|\bP79HD\b|\bP89s\b|\bA88\b|\bP10HD\b|\bP19HD\b|G18 3G|\bP78HD\b|\bA78\b|\bP75\b|G17s 3G|G17h 3G|\bP85t\b|\bP90\b|\bP11\b|\bP98t\b|\bP98HD\b|\bG18d\b|\bP85s\b|\bP11HD\b|\bP88s\b|\bA80HD\b|\bA80se\b|\bA10h\b|\bP89\b|\bP78s\b|\bG18\b|\bP85\b|\bA70h\b|\bA70\b|\bG17\b|\bP18\b|\bA80s\b|\bA11s\b|\bP88HD\b|\bA80h\b|\bP76s\b|\bP76h\b|\bP98\b|\bA10HD\b|\bP78\b|\bP88\b|\bA11\b|\bA10t\b|\bP76a\b|\bP76t\b|\bP76e\b|\bP85HD\b|\bP85a\b|\bP86\b|\bP75HD\b|\bP76v\b|\bA12\b|\bP75a\b|\bA15\b|\bP76Ti\b|\bP81HD\b|\bA10\b|\bT760VE\b|\bT720HD\b|\bP76\b|\bP73\b|\bP71\b|\bP72\b|\bT720SE\b|\bC520Ti\b|\bT760\b|\bT720VE\b|T720-3GE|T720-WiFi', // Onda - http://www.onda-tablet.com/buy-android-onda.html?dir=desc&limit=all&order=price 'OndaTablet' => '\b(V975i|Vi30|VX530|V701|Vi60|V701s|Vi50|V801s|V719|Vx610w|VX610W|V819i|Vi10|VX580W|Vi10|V711s|V813|V811|V820w|V820|Vi20|V711|VI30W|V712|V891w|V972|V819w|V820w|Vi60|V820w|V711|V813s|V801|V819|V975s|V801|V819|V819|V818|V811|V712|V975m|V101w|V961w|V812|V818|V971|V971s|V919|V989|V116w|V102w|V973|Vi40)\b[\s]+|V10 \b4G\b', 'JaytechTablet' => 'TPC-PA762', 'BlaupunktTablet' => 'Endeavour 800NG|Endeavour 1010', // http://www.digma.ru/support/download/ // @todo: Ebooks also (if requested) 'DigmaTablet' => '\b(iDx10|iDx9|iDx8|iDx7|iDxD7|iDxD8|iDsQ8|iDsQ7|iDsQ8|iDsD10|iDnD7|3TS804H|iDsQ11|iDj7|iDs10)\b', // http://www.evolioshop.com/ro/tablete-pc.html // http://www.evolio.ro/support/downloads_static.html?cat=2 // @todo: Research some more 'EvolioTablet' => 'ARIA_Mini_wifi|Aria[ _]Mini|Evolio X10|Evolio X7|Evolio X8|\bEvotab\b|\bNeura\b', // @todo http://www.lavamobiles.com/tablets-data-cards 'LavaTablet' => 'QPAD E704|\bIvoryS\b|E-TAB IVORY|\bE-TAB\b', // http://www.breezetablet.com/ 'AocTablet' => 'MW0811|MW0812|MW0922|MTK8382|MW1031|MW0831|MW0821|MW0931|MW0712', // http://www.mpmaneurope.com/en/products/internet-tablets-14/android-tablets-14/ 'MpmanTablet' => 'MP11 OCTA|MP10 OCTA|MPQC1114|MPQC1004|MPQC994|MPQC974|MPQC973|MPQC804|MPQC784|MPQC780|\bMPG7\b|MPDCG75|MPDCG71|MPDC1006|MP101DC|MPDC9000|MPDC905|MPDC706HD|MPDC706|MPDC705|MPDC110|MPDC100|MPDC99|MPDC97|MPDC88|MPDC8|MPDC77|MP709|MID701|MID711|MID170|MPDC703|MPQC1010', // https://www.celkonmobiles.com/?_a=categoryphones&sid=2 'CelkonTablet' => 'CT695|CT888|CT[\s]?910|CT7 Tab|CT9 Tab|CT3 Tab|CT2 Tab|CT1 Tab|C820|C720|\bCT-1\b', // http://www.wolderelectronics.com/productos/manuales-y-guias-rapidas/categoria-2-miTab 'WolderTablet' => 'miTab \b(DIAMOND|SPACE|BROOKLYN|NEO|FLY|MANHATTAN|FUNK|EVOLUTION|SKY|GOCAR|IRON|GENIUS|POP|MINT|EPSILON|BROADWAY|JUMP|HOP|LEGEND|NEW AGE|LINE|ADVANCE|FEEL|FOLLOW|LIKE|LINK|LIVE|THINK|FREEDOM|CHICAGO|CLEVELAND|BALTIMORE-GH|IOWA|BOSTON|SEATTLE|PHOENIX|DALLAS|IN 101|MasterChef)\b', 'MediacomTablet' => 'M-MPI10C3G|M-SP10EG|M-SP10EGP|M-SP10HXAH|M-SP7HXAH|M-SP10HXBH|M-SP8HXAH|M-SP8MXA', // http://www.mi.com/en 'MiTablet' => '\bMI PAD\b|\bHM NOTE 1W\b', // http://www.nbru.cn/index.html 'NibiruTablet' => 'Nibiru M1|Nibiru Jupiter One', // http://navroad.com/products/produkty/tablety/ // http://navroad.com/products/produkty/tablety/ 'NexoTablet' => 'NEXO NOVA|NEXO 10|NEXO AVIO|NEXO FREE|NEXO GO|NEXO EVO|NEXO 3G|NEXO SMART|NEXO KIDDO|NEXO MOBI', // http://leader-online.com/new_site/product-category/tablets/ // http://www.leader-online.net.au/List/Tablet 'LeaderTablet' => 'TBLT10Q|TBLT10I|TBL-10WDKB|TBL-10WDKBO2013|TBL-W230V2|TBL-W450|TBL-W500|SV572|TBLT7I|TBA-AC7-8G|TBLT79|TBL-8W16|TBL-10W32|TBL-10WKB|TBL-W100', // http://www.datawind.com/ubislate/ 'UbislateTablet' => 'UbiSlate[\s]?7C', // http://www.pocketbook-int.com/ru/support 'PocketBookTablet' => 'Pocketbook', // http://www.kocaso.com/product_tablet.html 'KocasoTablet' => '\b(TB-1207)\b', // http://global.hisense.com/product/asia/tablet/Sero7/201412/t20141215_91832.htm 'HisenseTablet' => '\b(F5281|E2371)\b', // http://www.tesco.com/direct/hudl/ 'Hudl' => 'Hudl HT7S3|Hudl 2', // http://www.telstra.com.au/home-phone/thub-2/ 'TelstraTablet' => 'T-Hub2', 'GenericTablet' => 'Android.*\b97D\b|Tablet(?!.*PC)|BNTV250A|MID-WCDMA|LogicPD Zoom2|\bA7EB\b|CatNova8|A1_07|CT704|CT1002|\bM721\b|rk30sdk|\bEVOTAB\b|M758A|ET904|ALUMIUM10|Smartfren Tab|Endeavour 1010|Tablet-PC-4|Tagi Tab|\bM6pro\b|CT1020W|arc 10HD|\bTP750\b|\bQTAQZ3\b|WVT101|TM1088|KT107' ]; /** * List of mobile Operating Systems. * * @var array */ protected static array $operatingSystems = [ 'AndroidOS' => 'Android', 'BlackBerryOS' => 'blackberry|\bBB10\b|rim tablet os', 'PalmOS' => 'PalmOS|avantgo|blazer|elaine|hiptop|palm|plucker|xiino', 'SymbianOS' => 'Symbian|SymbOS|Series60|Series40|SYB-[0-9]+|\bS60\b', // @reference: http://en.wikipedia.org/wiki/Windows_Mobile 'WindowsMobileOS' => 'Windows CE.*(PPC|Smartphone|Mobile|[0-9]{3}x[0-9]{3})|Windows Mobile|Windows Phone [0-9.]+|WCE;', // @reference: http://en.wikipedia.org/wiki/Windows_Phone // http://wifeng.cn/?r=blog&a=view&id=106 // http://nicksnettravels.builttoroam.com/post/2011/01/10/Bogus-Windows-Phone-7-User-Agent-String.aspx // http://msdn.microsoft.com/library/ms537503.aspx // https://msdn.microsoft.com/en-us/library/hh869301(v=vs.85).aspx 'WindowsPhoneOS' => 'Windows Phone 10.0|Windows Phone 8.1|Windows Phone 8.0|Windows Phone OS|XBLWP7|ZuneWP7|Windows NT 6.[23]; ARM;', 'iOS' => '\biPhone.*Mobile|\biPod|\biPad|AppleCoreMedia', // https://en.wikipedia.org/wiki/IPadOS 'iPadOS' => 'CPU OS 13', // @reference https://en.m.wikipedia.org/wiki/Sailfish_OS // https://sailfishos.org/ 'SailfishOS' => 'Sailfish', // http://en.wikipedia.org/wiki/MeeGo // @todo: research MeeGo in UAs 'MeeGoOS' => 'MeeGo', // http://en.wikipedia.org/wiki/Maemo // @todo: research Maemo in UAs 'MaemoOS' => 'Maemo', 'JavaOS' => 'J2ME/|\bMIDP\b|\bCLDC\b', // '|Java/' produces bug #135 'webOS' => 'webOS|hpwOS', 'badaOS' => '\bBada\b', 'BREWOS' => 'BREW', ]; /** * List of mobile User Agents. * * IMPORTANT: This is a list of only mobile browsers. * Mobile Detect 2.x supports only mobile browsers, * it was never designed to detect all browsers. * The change will come in 2017 in the 3.x release for PHP7. * * @var array */ protected static array $browsers = [ //'Vivaldi' => 'Vivaldi', // @reference: https://developers.google.com/chrome/mobile/docs/user-agent 'Chrome' => '\bCrMo\b|CriOS.*Mobile|Android.*Chrome/[.0-9]* Mobile', 'Dolfin' => '\bDolfin\b', 'Opera' => 'Opera.*Mini|Opera.*Mobi|Android.*Opera|Mobile.*OPR/[0-9.]+$|Coast/[0-9.]+', 'Skyfire' => 'Skyfire', // Added "Edge on iOS" https://github.com/serbanghita/Mobile-Detect/issues/764 'Edge' => 'EdgiOS.*Mobile|Mobile Safari/[.0-9]* Edge', 'IE' => 'IEMobile|MSIEMobile', // |Trident/[.0-9]+ 'Firefox' => 'fennec|firefox.*maemo|(Mobile|Tablet).*Firefox|Firefox.*Mobile|FxiOS.*Mobile', 'Bolt' => 'bolt', 'TeaShark' => 'teashark', 'Blazer' => 'Blazer', // @reference: http://developer.apple.com/library/safari/#documentation/AppleApplications/Reference/SafariWebContent/OptimizingforSafarioniPhone/OptimizingforSafarioniPhone.html#//apple_ref/doc/uid/TP40006517-SW3 // Excluded "Edge on iOS" https://github.com/serbanghita/Mobile-Detect/issues/764 'Safari' => 'Version((?!\bEdgiOS\b).)*Mobile.*Safari|Safari.*Mobile|MobileSafari', // http://en.wikipedia.org/wiki/Midori_(web_browser) //'Midori' => 'midori', //'Tizen' => 'Tizen', 'WeChat' => '\bMicroMessenger\b', 'UCBrowser' => 'UC.*Browser|UCWEB', 'baiduboxapp' => 'baiduboxapp', 'baidubrowser' => 'baidubrowser', // https://github.com/serbanghita/Mobile-Detect/issues/7 'DiigoBrowser' => 'DiigoBrowser', // http://www.puffinbrowser.com/index.php // https://github.com/serbanghita/Mobile-Detect/issues/752 // 'Puffin' => 'Puffin', // http://mercury-browser.com/index.html 'Mercury' => '\bMercury\b', // http://en.wikipedia.org/wiki/Obigo_Browser 'ObigoBrowser' => 'Obigo', // http://en.wikipedia.org/wiki/NetFront 'NetFront' => 'NF-Browser', // @reference: http://en.wikipedia.org/wiki/Minimo // http://en.wikipedia.org/wiki/Vision_Mobile_Browser 'GenericBrowser' => 'NokiaBrowser|OviBrowser|OneBrowser|TwonkyBeamBrowser|SEMC.*Browser|FlyFlow|Minimo|NetFront|Novarra-Vision|MQQBrowser|MicroMessenger', // @reference: https://en.wikipedia.org/wiki/Pale_Moon_(web_browser) 'PaleMoon' => 'Android.*PaleMoon|Mobile.*PaleMoon', ]; /** * All possible HTTP headers that represent the * User-Agent string. * * @var array */ protected static array $uaHttpHeaders = [ // The default User-Agent string. 'HTTP_USER_AGENT', // Header can occur on devices using Opera Mini. 'HTTP_X_OPERAMINI_PHONE_UA', // Vodafone specific header: http://www.seoprinciple.com/mobile-web-community-still-angry-at-vodafone/24/ 'HTTP_X_DEVICE_USER_AGENT', 'HTTP_X_ORIGINAL_USER_AGENT', 'HTTP_X_SKYFIRE_PHONE', 'HTTP_X_BOLT_PHONE_UA', 'HTTP_DEVICE_STOCK_UA', 'HTTP_X_UCBROWSER_DEVICE_UA' ]; /** * The individual segments that could exist in a User-Agent string. VER refers to the regular * expression defined in the constant self::VER. * * @var array */ protected static array $properties = [ // Build 'Mobile' => 'Mobile/[VER]', 'Build' => 'Build/[VER]', 'Version' => 'Version/[VER]', 'VendorID' => 'VendorID/[VER]', // Devices 'iPad' => 'iPad.*CPU[a-z ]+[VER]', 'iPhone' => 'iPhone.*CPU[a-z ]+[VER]', 'iPod' => 'iPod.*CPU[a-z ]+[VER]', //'BlackBerry' => array('BlackBerry[VER]', 'BlackBerry [VER];'), 'Kindle' => 'Kindle/[VER]', // Browser 'Chrome' => ['Chrome/[VER]', 'CriOS/[VER]', 'CrMo/[VER]'], 'Coast' => ['Coast/[VER]'], 'Dolfin' => 'Dolfin/[VER]', // @reference: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent/Firefox 'Firefox' => ['Firefox/[VER]', 'FxiOS/[VER]'], 'Fennec' => 'Fennec/[VER]', // http://msdn.microsoft.com/en-us/library/ms537503(v=vs.85).aspx // https://msdn.microsoft.com/en-us/library/ie/hh869301(v=vs.85).aspx 'Edge' => 'Edge/[VER]', 'IE' => ['IEMobile/[VER];', 'IEMobile [VER]', 'MSIE [VER];', 'Trident/[0-9.]+;.*rv:[VER]'], // http://en.wikipedia.org/wiki/NetFront 'NetFront' => 'NetFront/[VER]', 'NokiaBrowser' => 'NokiaBrowser/[VER]', 'Opera' => [' OPR/[VER]', 'Opera Mini/[VER]', 'Version/[VER]'], 'Opera Mini' => 'Opera Mini/[VER]', 'Opera Mobi' => 'Version/[VER]', 'UCBrowser' => ['UCWEB[VER]', 'UC.*Browser/[VER]'], 'MQQBrowser' => 'MQQBrowser/[VER]', 'MicroMessenger' => 'MicroMessenger/[VER]', 'baiduboxapp' => 'baiduboxapp/[VER]', 'baidubrowser' => 'baidubrowser/[VER]', 'SamsungBrowser' => 'SamsungBrowser/[VER]', 'Iron' => 'Iron/[VER]', // @note: Safari 7534.48.3 is actually Version 5.1. // @note: On BlackBerry the Version is overwriten by the OS. 'Safari' => ['Version/[VER]', 'Safari/[VER]'], 'Skyfire' => 'Skyfire/[VER]', 'Tizen' => 'Tizen/[VER]', 'Webkit' => 'webkit[ /][VER]', 'PaleMoon' => 'PaleMoon/[VER]', 'SailfishBrowser' => 'SailfishBrowser/[VER]', // Engine 'Gecko' => 'Gecko/[VER]', 'Trident' => 'Trident/[VER]', 'Presto' => 'Presto/[VER]', 'Goanna' => 'Goanna/[VER]', // OS 'iOS' => ' \bi?OS\b [VER][ ;]{1}', 'Android' => 'Android [VER]', 'Sailfish' => 'Sailfish [VER]', 'BlackBerry' => ['BlackBerry[\w]+/[VER]', 'BlackBerry.*Version/[VER]', 'Version/[VER]'], 'BREW' => 'BREW [VER]', 'Java' => 'Java/[VER]', // @reference: http://windowsteamblog.com/windows_phone/b/wpdev/archive/2011/08/29/introducing-the-ie9-on-windows-phone-mango-user-agent-string.aspx // @reference: http://en.wikipedia.org/wiki/Windows_NT#Releases 'Windows Phone OS' => ['Windows Phone OS [VER]', 'Windows Phone [VER]'], 'Windows Phone' => 'Windows Phone [VER]', 'Windows CE' => 'Windows CE/[VER]', // http://social.msdn.microsoft.com/Forums/en-US/windowsdeveloperpreviewgeneral/thread/6be392da-4d2f-41b4-8354-8dcee20c85cd 'Windows NT' => 'Windows NT [VER]', 'Symbian' => ['SymbianOS/[VER]', 'Symbian/[VER]'], 'webOS' => ['webOS/[VER]', 'hpwOS/[VER];'], ]; /** * Construct an instance of this class. * * @param array|null $headers Specify the headers as injection. Should be PHP _SERVER flavored. * If left empty, will use the global _SERVER['HTTP_*'] vars instead. * @param string|null $userAgent Inject the User-Agent header. If null, will use HTTP_USER_AGENT * from the $headers array instead. */ public function __construct(?array $headers = null, ?string $userAgent = null) { $this->setHttpHeaders($headers); $this->setUserAgent($userAgent); } /** * Get the current script version. * This is useful for the demo.php file, * so people can check on what version they are testing * for mobile devices. * * @return string The version number in semantic version format. */ public static function getScriptVersion(): string { return self::VERSION; } /** * Set the HTTP Headers. Must be PHP-flavored. This method will reset existing headers. * * @param array|null $httpHeaders The headers to set. If null, then using PHP's _SERVER to extract * the headers. The default null is left for backwards compatibility. */ public function setHttpHeaders(?array $httpHeaders = null) { // use global _SERVER if $httpHeaders aren't defined if (!is_array($httpHeaders) || !count($httpHeaders)) { $httpHeaders = $_SERVER; } // clear existing headers $this->httpHeaders = array(); // Only save HTTP headers. In PHP land, that means only _SERVER vars that // start with HTTP_. foreach ($httpHeaders as $key => $value) { if (substr($key, 0, 5) === 'HTTP_') { $this->httpHeaders[$key] = $value; } } // In case we're dealing with CloudFront, we need to know. $this->setCfHeaders($httpHeaders); } /** * Retrieves the HTTP headers. * * @return array */ public function getHttpHeaders(): array { return $this->httpHeaders; } /** * Retrieves a particular header. If it doesn't exist, no exception/error is caused. * Simply null is returned. * * @param string $header The name of the header to retrieve. Can be HTTP compliant such as * "User-Agent" or "X-Device-User-Agent" or can be php-esque with the * all-caps, HTTP_ prefixed, underscore separated awesomeness. * * @return string|null The value of the header. */ public function getHttpHeader(string $header): ?string { // are we using PHP-flavored headers? if (strpos($header, '_') === false) { $header = str_replace('-', '_', $header); $header = strtoupper($header); } // test the alternate, too $altHeader = 'HTTP_' . $header; //Test both the regular and the HTTP_ prefix if (isset($this->httpHeaders[$header])) { return $this->httpHeaders[$header]; } elseif (isset($this->httpHeaders[$altHeader])) { return $this->httpHeaders[$altHeader]; } return null; } public function getMobileHeaders(): array { return static::$mobileHeaders; } /** * Get all possible HTTP headers that * can contain the User-Agent string. * * @return array List of HTTP headers. */ public function getUaHttpHeaders(): array { return static::$uaHttpHeaders; } /** * Set CloudFront headers * http://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/header-caching.html#header-caching-web-device * * @param array|null $cfHeaders List of HTTP headers * * @return bool If there were CloudFront headers to be set */ public function setCfHeaders(?array $cfHeaders = null): bool { // use global _SERVER if $cfHeaders aren't defined if (!is_array($cfHeaders) || !count($cfHeaders)) { $cfHeaders = $_SERVER; } // clear existing headers $this->cloudfrontHeaders = array(); // Only save CLOUDFRONT headers. In PHP land, that means only _SERVER vars that // start with cloudfront-. $response = false; foreach ($cfHeaders as $key => $value) { if (substr(strtolower($key), 0, 16) === 'http_cloudfront_') { $this->cloudfrontHeaders[strtoupper($key)] = $value; $response = true; } } return $response; } /** * Retrieves the cloudfront headers. * * @return array */ public function getCfHeaders(): array { return $this->cloudfrontHeaders; } /** * @param string $userAgent * @return string */ private function prepareUserAgent(string $userAgent): string { $userAgent = trim($userAgent); return substr($userAgent, 0, 500); } /** * Set the User-Agent to be used. * * @param string|null $userAgent The user agent string to set. * * @return string|null */ public function setUserAgent(?string $userAgent = null): ?string { // Invalidate cache due to #375 $this->cache = array(); if (false === empty($userAgent)) { return $this->userAgent = $this->prepareUserAgent($userAgent); } else { $this->userAgent = null; foreach ($this->getUaHttpHeaders() as $altHeader) { // @todo: should use getHttpHeader(), but it would be slow. (Serban) if (false === empty($this->httpHeaders[$altHeader])) { $this->userAgent .= $this->httpHeaders[$altHeader] . " "; } } if (!empty($this->userAgent)) { return $this->userAgent = $this->prepareUserAgent($this->userAgent); } } if (count($this->getCfHeaders()) > 0) { return $this->userAgent = 'Amazon CloudFront'; } return $this->userAgent = null; } /** * Retrieve the User-Agent. * * @return string|null The user agent if it's set. */ public function getUserAgent(): ?string { return $this->userAgent; } public function getMatchingRegex(): ?string { return $this->matchingRegex; } public function getMatchesArray(): ?array { return $this->matchesArray; } /** * Retrieve the list of known phone devices. * * @return array List of phone devices. */ public static function getPhoneDevices(): array { return static::$phoneDevices; } /** * Retrieve the list of known tablet devices. * * @return array List of tablet devices. */ public static function getTabletDevices(): array { return static::$tabletDevices; } /** * Alias for getBrowsers() method. * * @return array List of user agents. */ public static function getUserAgents(): array { return static::getBrowsers(); } /** * Retrieve the list of known browsers. Specifically, the user agents. * * @return array List of browsers / user agents. */ public static function getBrowsers(): array { return static::$browsers; } /** * Method gets the mobile detection rules. This method is used for the magic methods $detect->is*(). * Retrieve the current set of rules. * * @return array */ public function getRules(): array { static $rules; if (!$rules) { $rules = array_merge( static::$phoneDevices, static::$tabletDevices, static::$operatingSystems, static::$browsers ); } return $rules; } /** * Retrieve the list of mobile operating systems. * * @return array The list of mobile operating systems. */ public static function getOperatingSystems(): array { return static::$operatingSystems; } /** * Check the HTTP headers for signs of mobile. * This is the fastest mobile check possible; it's used * inside isMobile() method. * * @return bool */ public function checkHttpHeadersForMobile(): bool { foreach ($this->getMobileHeaders() as $mobileHeader => $matchType) { if (isset($this->httpHeaders[$mobileHeader])) { if (isset($matchType['matches']) && is_array($matchType['matches'])) { foreach ($matchType['matches'] as $_match) { if (strpos($this->httpHeaders[$mobileHeader], $_match) !== false) { return true; } } return false; } else { return true; } } } return false; } /** * Magic overloading method. * * @method boolean is[...]() * @param string $name * @param array $arguments * @return bool * @throws BadMethodCallException when the method doesn't exist and doesn't start with 'is' */ public function __call(string $name, array $arguments) { // make sure the name starts with 'is', otherwise if (substr($name, 0, 2) !== 'is') { throw new BadMethodCallException("No such method exists: $name"); } $key = substr($name, 2); return $this->matchUAAgainstKey($key); } /** * Find a detection rule that matches the current User-agent. * * @param string|null $userAgent deprecated * @return bool */ protected function matchDetectionRulesAgainstUA(?string $userAgent = null): bool { // Begin general search. foreach ($this->getRules() as $_regex) { if (empty($_regex)) { continue; } if ($this->match($_regex, $userAgent)) { return true; } } return false; } /** * Search for a certain key in the rules array. * If the key is found then try to match the corresponding * regex against the User-Agent. * * @param string $key * * @return bool */ protected function matchUAAgainstKey(string $key): bool { // Make the keys lowercase, so we can match: isIphone(), isiPhone(), isiphone(), etc. $key = strtolower($key); if (false === isset($this->cache[$key])) { // change the keys to lower case $_rules = array_change_key_case($this->getRules()); if (false === empty($_rules[$key])) { $this->cache[$key] = $this->match($_rules[$key]); } if (false === isset($this->cache[$key])) { $this->cache[$key] = false; } } return $this->cache[$key]; } /** * Check if the device is mobile. * Returns true if any type of mobile device detected, including special ones * @param string|null $userAgent deprecated * @param array|null $httpHeaders deprecated * @return bool */ public function isMobile(?string $userAgent = null, ?array $httpHeaders = null): bool { if ($httpHeaders) { $this->setHttpHeaders($httpHeaders); } if ($userAgent) { $this->setUserAgent($userAgent); } // Check specifically for cloudfront headers if the useragent === 'Amazon CloudFront' if ($this->getUserAgent() === 'Amazon CloudFront') { $cfHeaders = $this->getCfHeaders(); if (array_key_exists('HTTP_CLOUDFRONT_IS_MOBILE_VIEWER', $cfHeaders) && $cfHeaders['HTTP_CLOUDFRONT_IS_MOBILE_VIEWER'] === 'true' ) { return true; } } if ($this->checkHttpHeadersForMobile()) { return true; } else { return $this->matchDetectionRulesAgainstUA(); } } /** * Check if the device is a tablet. * Return true if any type of tablet device is detected. * * @param string|null $userAgent deprecated * @param array|null $httpHeaders deprecated * @return bool */ public function isTablet(?string $userAgent = null, ?array $httpHeaders = null): bool { // Check specifically for cloudfront headers if the useragent === 'Amazon CloudFront' if ($this->getUserAgent() === 'Amazon CloudFront') { $cfHeaders = $this->getCfHeaders(); if (array_key_exists('HTTP_CLOUDFRONT_IS_TABLET_VIEWER', $cfHeaders) && $cfHeaders['HTTP_CLOUDFRONT_IS_TABLET_VIEWER'] === 'true' ) { return true; } } foreach (static::$tabletDevices as $_regex) { if ($this->match($_regex, $userAgent)) { return true; } } return false; } /** * This method checks for a certain property in the * userAgent. * @param string $key * @param string|null $userAgent deprecated * @param array|null $httpHeaders deprecated * @return bool * * @todo: The httpHeaders part is not yet used. */ public function is(string $key, ?string $userAgent = null, ?array $httpHeaders = null): bool { // Set the UA and HTTP headers only if needed (eg. batch mode). if ($httpHeaders) { $this->setHttpHeaders($httpHeaders); } if ($userAgent) { $this->setUserAgent($userAgent); } return $this->matchUAAgainstKey($key); } /** * Some detection rules are relative (not standard), * because of the diversity of devices, vendors and * their conventions in representing the User-Agent or * the HTTP headers. * * This method will be used to check custom regexes against * the User-Agent string. * * @param string $regex * @param string|null $userAgent * @return bool * * @todo: search in the HTTP headers too. */ public function match(string $regex, ?string $userAgent = null): bool { if (!\is_string($userAgent) && !\is_string($this->userAgent)) { return false; } $match = (bool) preg_match( sprintf('#%s#is', $regex), (false === empty($userAgent) ? $userAgent : $this->userAgent), $matches ); // If positive match is found, store the results for debug. if ($match) { $this->matchingRegex = $regex; $this->matchesArray = $matches; } return $match; } /** * Get the properties array. * * @return array */ public static function getProperties(): array { return static::$properties; } /** * Prepare the version number. * * @param string $ver The string version, like "2.6.21.2152"; * * @return float * * @todo Remove the error suppression from str_replace() call. */ public function prepareVersionNo(string $ver): float { $ver = str_replace(array('_', ' ', '/'), '.', $ver); $arrVer = explode('.', $ver, 2); if (isset($arrVer[1])) { $arrVer[1] = @str_replace('.', '', $arrVer[1]); // @todo: treat strings versions. } return (float) implode('.', $arrVer); } /** * Check the version of the given property in the User-Agent. * Will return a float number. (e.g. 2_0 will return 2.0, 4.3.1 will return 4.31) * * @param string $propertyName The name of the property. See self::getProperties() array * keys for all possible properties. * @param string $type Either self::VERSION_TYPE_STRING to get a string value or * self::VERSION_TYPE_FLOAT indicating a float value. This parameter * is optional and defaults to self::VERSION_TYPE_STRING. Passing an * invalid parameter will default to the type as well. * * @return string|float|false The version of the property we are trying to extract. */ public function version(string $propertyName, string $type = self::VERSION_TYPE_STRING) { if (empty($propertyName)) { return false; } if (!\is_string($this->userAgent)) { return false; } // set the $type to the default if we don't recognize the type if ($type !== self::VERSION_TYPE_STRING && $type !== self::VERSION_TYPE_FLOAT) { $type = self::VERSION_TYPE_STRING; } $properties = self::getProperties(); // Check if the property exists in the properties array. if (true === isset($properties[$propertyName])) { // Prepare the pattern to be matched. // Make sure we always deal with an array (string is converted). $properties[$propertyName] = (array) $properties[$propertyName]; foreach ($properties[$propertyName] as $propertyMatchString) { $propertyPattern = str_replace('[VER]', self::VER, $propertyMatchString); // Identify and extract the version. preg_match(sprintf('#%s#is', $propertyPattern), $this->userAgent, $match); if (false === empty($match[1])) { return ($type == self::VERSION_TYPE_FLOAT ? $this->prepareVersionNo($match[1]) : $match[1]); } } } return false; } } packages/Sabberworm/CSS/CSSList/AtRuleBlockList.php 0000644 00000003623 15217646627 0016101 0 ustar 00 <?php namespace Automattic\WooCommerce\Vendor\Sabberworm\CSS\CSSList; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\OutputFormat; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Property\AtRule; /** * A `BlockList` constructed by an unknown at-rule. `@media` rules are rendered into `AtRuleBlockList` objects. */ class AtRuleBlockList extends CSSBlockList implements AtRule { /** * @var string */ private $sType; /** * @var string */ private $sArgs; /** * @param string $sType * @param string $sArgs * @param int $iLineNo */ public function __construct($sType, $sArgs = '', $iLineNo = 0) { parent::__construct($iLineNo); $this->sType = $sType; $this->sArgs = $sArgs; } /** * @return string */ public function atRuleName() { return $this->sType; } /** * @return string */ public function atRuleArgs() { return $this->sArgs; } /** * @return string * * @deprecated in V8.8.0, will be removed in V9.0.0. Use `render` instead. */ public function __toString() { return $this->render(new OutputFormat()); } /** * @param OutputFormat|null $oOutputFormat * * @return string */ public function render($oOutputFormat) { $sResult = $oOutputFormat->comments($this); $sResult .= $oOutputFormat->sBeforeAtRuleBlock; $sArgs = $this->sArgs; if ($sArgs) { $sArgs = ' ' . $sArgs; } $sResult .= "@{$this->sType}$sArgs{$oOutputFormat->spaceBeforeOpeningBrace()}{"; $sResult .= $this->renderListContents($oOutputFormat); $sResult .= '}'; $sResult .= $oOutputFormat->sAfterAtRuleBlock; return $sResult; } /** * @return bool */ public function isRootList() { return false; } } packages/Sabberworm/CSS/CSSList/CSSList.php 0000644 00000041153 15217646627 0014362 0 ustar 00 <?php namespace Automattic\WooCommerce\Vendor\Sabberworm\CSS\CSSList; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Comment\Comment; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Comment\Commentable; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\CSSElement; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\OutputFormat; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Parsing\ParserState; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Parsing\SourceException; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Parsing\UnexpectedEOFException; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Parsing\UnexpectedTokenException; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Position\Position; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Position\Positionable; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Property\AtRule; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Property\Charset; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Property\CSSNamespace; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Property\Import; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Property\Selector; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Renderable; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\RuleSet\AtRuleSet; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\RuleSet\DeclarationBlock; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\RuleSet\RuleSet; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Settings; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Value\CSSString; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Value\URL; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Value\Value; /** * This is the most generic container available. It can contain `DeclarationBlock`s (rule sets with a selector), * `RuleSet`s as well as other `CSSList` objects. * * It can also contain `Import` and `Charset` objects stemming from at-rules. */ abstract class CSSList implements Commentable, CSSElement, Positionable { use Position; /** * @var array<array-key, Comment> * * @internal since 8.8.0 */ protected $aComments; /** * @var array<int, RuleSet|CSSList|Import|Charset> * * @internal since 8.8.0 */ protected $aContents; /** * @param int $iLineNo */ public function __construct($iLineNo = 0) { $this->aComments = []; $this->aContents = []; $this->setPosition($iLineNo); } /** * @return void * * @throws UnexpectedTokenException * @throws SourceException * * @internal since V8.8.0 */ public static function parseList(ParserState $oParserState, CSSList $oList) { $bIsRoot = $oList instanceof Document; if (is_string($oParserState)) { $oParserState = new ParserState($oParserState, Settings::create()); } $bLenientParsing = $oParserState->getSettings()->bLenientParsing; $aComments = []; while (!$oParserState->isEnd()) { $aComments = array_merge($aComments, $oParserState->consumeWhiteSpace()); $oListItem = null; if ($bLenientParsing) { try { $oListItem = self::parseListItem($oParserState, $oList); } catch (UnexpectedTokenException $e) { $oListItem = false; } } else { $oListItem = self::parseListItem($oParserState, $oList); } if ($oListItem === null) { // List parsing finished return; } if ($oListItem) { $oListItem->addComments($aComments); $oList->append($oListItem); } $aComments = $oParserState->consumeWhiteSpace(); } $oList->addComments($aComments); if (!$bIsRoot && !$bLenientParsing) { throw new SourceException("Unexpected end of document", $oParserState->currentLine()); } } /** * @return AtRuleBlockList|KeyFrame|Charset|CSSNamespace|Import|AtRuleSet|DeclarationBlock|null|false * * @throws SourceException * @throws UnexpectedEOFException * @throws UnexpectedTokenException */ private static function parseListItem(ParserState $oParserState, CSSList $oList) { $bIsRoot = $oList instanceof Document; if ($oParserState->comes('@')) { $oAtRule = self::parseAtRule($oParserState); if ($oAtRule instanceof Charset) { if (!$bIsRoot) { throw new UnexpectedTokenException( '@charset may only occur in root document', '', 'custom', $oParserState->currentLine() ); } if (count($oList->getContents()) > 0) { throw new UnexpectedTokenException( '@charset must be the first parseable token in a document', '', 'custom', $oParserState->currentLine() ); } $oParserState->setCharset($oAtRule->getCharset()); } return $oAtRule; } elseif ($oParserState->comes('}')) { if ($bIsRoot) { if ($oParserState->getSettings()->bLenientParsing) { return DeclarationBlock::parse($oParserState); } else { throw new SourceException("Unopened {", $oParserState->currentLine()); } } else { // End of list return null; } } else { return DeclarationBlock::parse($oParserState, $oList); } } /** * @param ParserState $oParserState * * @return AtRuleBlockList|KeyFrame|Charset|CSSNamespace|Import|AtRuleSet|null * * @throws SourceException * @throws UnexpectedTokenException * @throws UnexpectedEOFException */ private static function parseAtRule(ParserState $oParserState) { $oParserState->consume('@'); $sIdentifier = $oParserState->parseIdentifier(); $iIdentifierLineNum = $oParserState->currentLine(); $oParserState->consumeWhiteSpace(); if ($sIdentifier === 'import') { $oLocation = URL::parse($oParserState); $oParserState->consumeWhiteSpace(); $sMediaQuery = null; if (!$oParserState->comes(';')) { $sMediaQuery = trim($oParserState->consumeUntil([';', ParserState::EOF])); } $oParserState->consumeUntil([';', ParserState::EOF], true, true); return new Import($oLocation, $sMediaQuery ?: null, $iIdentifierLineNum); } elseif ($sIdentifier === 'charset') { $oCharsetString = CSSString::parse($oParserState); $oParserState->consumeWhiteSpace(); $oParserState->consumeUntil([';', ParserState::EOF], true, true); return new Charset($oCharsetString, $iIdentifierLineNum); } elseif (self::identifierIs($sIdentifier, 'keyframes')) { $oResult = new KeyFrame($iIdentifierLineNum); $oResult->setVendorKeyFrame($sIdentifier); $oResult->setAnimationName(trim($oParserState->consumeUntil('{', false, true))); CSSList::parseList($oParserState, $oResult); if ($oParserState->comes('}')) { $oParserState->consume('}'); } return $oResult; } elseif ($sIdentifier === 'namespace') { $sPrefix = null; $mUrl = Value::parsePrimitiveValue($oParserState); if (!$oParserState->comes(';')) { $sPrefix = $mUrl; $mUrl = Value::parsePrimitiveValue($oParserState); } $oParserState->consumeUntil([';', ParserState::EOF], true, true); if ($sPrefix !== null && !is_string($sPrefix)) { throw new UnexpectedTokenException('Wrong namespace prefix', $sPrefix, 'custom', $iIdentifierLineNum); } if (!($mUrl instanceof CSSString || $mUrl instanceof URL)) { throw new UnexpectedTokenException( 'Wrong namespace url of invalid type', $mUrl, 'custom', $iIdentifierLineNum ); } return new CSSNamespace($mUrl, $sPrefix, $iIdentifierLineNum); } else { // Unknown other at rule (font-face or such) $sArgs = trim($oParserState->consumeUntil('{', false, true)); if (substr_count($sArgs, "(") != substr_count($sArgs, ")")) { if ($oParserState->getSettings()->bLenientParsing) { return null; } else { throw new SourceException("Unmatched brace count in media query", $oParserState->currentLine()); } } $bUseRuleSet = true; foreach (explode('/', AtRule::BLOCK_RULES) as $sBlockRuleName) { if (self::identifierIs($sIdentifier, $sBlockRuleName)) { $bUseRuleSet = false; break; } } if ($bUseRuleSet) { $oAtRule = new AtRuleSet($sIdentifier, $sArgs, $iIdentifierLineNum); RuleSet::parseRuleSet($oParserState, $oAtRule); } else { $oAtRule = new AtRuleBlockList($sIdentifier, $sArgs, $iIdentifierLineNum); CSSList::parseList($oParserState, $oAtRule); if ($oParserState->comes('}')) { $oParserState->consume('}'); } } return $oAtRule; } } /** * Tests an identifier for a given value. Since identifiers are all keywords, they can be vendor-prefixed. * We need to check for these versions too. * * @param string $sIdentifier * @param string $sMatch * * @return bool */ private static function identifierIs($sIdentifier, $sMatch) { return (strcasecmp($sIdentifier, $sMatch) === 0) ?: preg_match("/^(-\\w+-)?$sMatch$/i", $sIdentifier) === 1; } /** * Prepends an item to the list of contents. * * @param RuleSet|CSSList|Import|Charset $oItem * * @return void */ public function prepend($oItem) { array_unshift($this->aContents, $oItem); } /** * Appends an item to the list of contents. * * @param RuleSet|CSSList|Import|Charset $oItem * * @return void */ public function append($oItem) { $this->aContents[] = $oItem; } /** * Splices the list of contents. * * @param int $iOffset * @param int $iLength * @param array<int, RuleSet|CSSList|Import|Charset> $mReplacement * * @return void */ public function splice($iOffset, $iLength = null, $mReplacement = null) { array_splice($this->aContents, $iOffset, $iLength, $mReplacement); } /** * Inserts an item in the CSS list before its sibling. If the desired sibling cannot be found, * the item is appended at the end. * * @param RuleSet|CSSList|Import|Charset $item * @param RuleSet|CSSList|Import|Charset $sibling */ public function insertBefore($item, $sibling) { if (in_array($sibling, $this->aContents, true)) { $this->replace($sibling, [$item, $sibling]); } else { $this->append($item); } } /** * Removes an item from the CSS list. * * @param RuleSet|Import|Charset|CSSList $oItemToRemove * May be a RuleSet (most likely a DeclarationBlock), a Import, * a Charset or another CSSList (most likely a MediaQuery) * * @return bool whether the item was removed */ public function remove($oItemToRemove) { $iKey = array_search($oItemToRemove, $this->aContents, true); if ($iKey !== false) { unset($this->aContents[$iKey]); return true; } return false; } /** * Replaces an item from the CSS list. * * @param RuleSet|Import|Charset|CSSList $oOldItem * May be a `RuleSet` (most likely a `DeclarationBlock`), an `Import`, a `Charset` * or another `CSSList` (most likely a `MediaQuery`) * * @return bool */ public function replace($oOldItem, $mNewItem) { $iKey = array_search($oOldItem, $this->aContents, true); if ($iKey !== false) { if (is_array($mNewItem)) { array_splice($this->aContents, $iKey, 1, $mNewItem); } else { array_splice($this->aContents, $iKey, 1, [$mNewItem]); } return true; } return false; } /** * @param array<int, RuleSet|Import|Charset|CSSList> $aContents */ public function setContents(array $aContents) { $this->aContents = []; foreach ($aContents as $content) { $this->append($content); } } /** * Removes a declaration block from the CSS list if it matches all given selectors. * * @param DeclarationBlock|array<array-key, Selector>|string $mSelector the selectors to match * @param bool $bRemoveAll whether to stop at the first declaration block found or remove all blocks * * @return void */ public function removeDeclarationBlockBySelector($mSelector, $bRemoveAll = false) { if ($mSelector instanceof DeclarationBlock) { $mSelector = $mSelector->getSelectors(); } if (!is_array($mSelector)) { $mSelector = explode(',', $mSelector); } foreach ($mSelector as $iKey => &$mSel) { if (!($mSel instanceof Selector)) { if (!Selector::isValid($mSel)) { throw new UnexpectedTokenException( "Selector did not match '" . Selector::SELECTOR_VALIDATION_RX . "'.", $mSel, "custom" ); } $mSel = new Selector($mSel); } } foreach ($this->aContents as $iKey => $mItem) { if (!($mItem instanceof DeclarationBlock)) { continue; } if ($mItem->getSelectors() == $mSelector) { unset($this->aContents[$iKey]); if (!$bRemoveAll) { return; } } } } /** * @return string * * @deprecated in V8.8.0, will be removed in V9.0.0. Use `render` instead. */ public function __toString() { return $this->render(new OutputFormat()); } /** * @return string */ protected function renderListContents(OutputFormat $oOutputFormat) { $sResult = ''; $bIsFirst = true; $oNextLevel = $oOutputFormat; if (!$this->isRootList()) { $oNextLevel = $oOutputFormat->nextLevel(); } foreach ($this->aContents as $oContent) { $sRendered = $oOutputFormat->safely(function () use ($oNextLevel, $oContent) { return $oContent->render($oNextLevel); }); if ($sRendered === null) { continue; } if ($bIsFirst) { $bIsFirst = false; $sResult .= $oNextLevel->spaceBeforeBlocks(); } else { $sResult .= $oNextLevel->spaceBetweenBlocks(); } $sResult .= $sRendered; } if (!$bIsFirst) { // Had some output $sResult .= $oOutputFormat->spaceAfterBlocks(); } return $sResult; } /** * Return true if the list can not be further outdented. Only important when rendering. * * @return bool */ abstract public function isRootList(); /** * Returns the stored items. * * @return array<int, RuleSet|Import|Charset|CSSList> */ public function getContents() { return $this->aContents; } /** * @param array<array-key, Comment> $aComments * * @return void */ public function addComments(array $aComments) { $this->aComments = array_merge($this->aComments, $aComments); } /** * @return array<array-key, Comment> */ public function getComments() { return $this->aComments; } /** * @param array<array-key, Comment> $aComments * * @return void */ public function setComments(array $aComments) { $this->aComments = $aComments; } } packages/Sabberworm/CSS/CSSList/Document.php 0000644 00000010454 15217646627 0014654 0 ustar 00 <?php namespace Automattic\WooCommerce\Vendor\Sabberworm\CSS\CSSList; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\OutputFormat; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Parsing\ParserState; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Parsing\SourceException; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Property\Selector; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\RuleSet\DeclarationBlock; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\RuleSet\RuleSet; /** * This class represents the root of a parsed CSS file. It contains all top-level CSS contents: mostly declaration * blocks, but also any at-rules encountered (`Import` and `Charset`). */ class Document extends CSSBlockList { /** * @param int $iLineNo */ public function __construct($iLineNo = 0) { parent::__construct($iLineNo); } /** * @return Document * * @throws SourceException * * @internal since V8.8.0 */ public static function parse(ParserState $oParserState) { $oDocument = new Document($oParserState->currentLine()); CSSList::parseList($oParserState, $oDocument); return $oDocument; } /** * Gets all `DeclarationBlock` objects recursively, no matter how deeply nested the selectors are. * Aliased as `getAllSelectors()`. * * @return array<int, DeclarationBlock> */ public function getAllDeclarationBlocks() { /** @var array<int, DeclarationBlock> $aResult */ $aResult = []; $this->allDeclarationBlocks($aResult); return $aResult; } /** * Gets all `DeclarationBlock` objects recursively. * * @return array<int, DeclarationBlock> * * @deprecated will be removed in version 9.0; use `getAllDeclarationBlocks()` instead */ public function getAllSelectors() { return $this->getAllDeclarationBlocks(); } /** * Returns all `RuleSet` objects recursively found in the tree, no matter how deeply nested the rule sets are. * * @return array<int, RuleSet> */ public function getAllRuleSets() { /** @var array<int, RuleSet> $aResult */ $aResult = []; $this->allRuleSets($aResult); return $aResult; } /** * Returns all `Selector` objects with the requested specificity found recursively in the tree. * * Note that this does not yield the full `DeclarationBlock` that the selector belongs to * (and, currently, there is no way to get to that). * * @param string|null $sSpecificitySearch * An optional filter by specificity. * May contain a comparison operator and a number or just a number (defaults to "=="). * * @return array<int, Selector> * @example `getSelectorsBySpecificity('>= 100')` * */ public function getSelectorsBySpecificity($sSpecificitySearch = null) { /** @var array<int, Selector> $aResult */ $aResult = []; $this->allSelectors($aResult, $sSpecificitySearch); return $aResult; } /** * Expands all shorthand properties to their long value. * * @return void * * @deprecated since 8.7.0, will be removed without substitution in version 9.0 in #511 */ public function expandShorthands() { foreach ($this->getAllDeclarationBlocks() as $oDeclaration) { $oDeclaration->expandShorthands(); } } /** * Create shorthands properties whenever possible. * * @return void * * @deprecated since 8.7.0, will be removed without substitution in version 9.0 in #511 */ public function createShorthands() { foreach ($this->getAllDeclarationBlocks() as $oDeclaration) { $oDeclaration->createShorthands(); } } /** * Overrides `render()` to make format argument optional. * * @param OutputFormat|null $oOutputFormat * * @return string */ public function render($oOutputFormat = null) { if ($oOutputFormat === null) { $oOutputFormat = new OutputFormat(); } return $oOutputFormat->comments($this) . $this->renderListContents($oOutputFormat); } /** * @return bool */ public function isRootList() { return true; } } packages/Sabberworm/CSS/CSSList/KeyFrame.php 0000644 00000004247 15217646627 0014604 0 ustar 00 <?php namespace Automattic\WooCommerce\Vendor\Sabberworm\CSS\CSSList; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\OutputFormat; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Property\AtRule; class KeyFrame extends CSSList implements AtRule { /** * @var string|null */ private $vendorKeyFrame; /** * @var string|null */ private $animationName; /** * @param int $iLineNo */ public function __construct($iLineNo = 0) { parent::__construct($iLineNo); $this->vendorKeyFrame = null; $this->animationName = null; } /** * @param string $vendorKeyFrame */ public function setVendorKeyFrame($vendorKeyFrame) { $this->vendorKeyFrame = $vendorKeyFrame; } /** * @return string|null */ public function getVendorKeyFrame() { return $this->vendorKeyFrame; } /** * @param string $animationName */ public function setAnimationName($animationName) { $this->animationName = $animationName; } /** * @return string|null */ public function getAnimationName() { return $this->animationName; } /** * @return string * * @deprecated in V8.8.0, will be removed in V9.0.0. Use `render` instead. */ public function __toString() { return $this->render(new OutputFormat()); } /** * @param OutputFormat|null $oOutputFormat * * @return string */ public function render($oOutputFormat) { $sResult = $oOutputFormat->comments($this); $sResult .= "@{$this->vendorKeyFrame} {$this->animationName}{$oOutputFormat->spaceBeforeOpeningBrace()}{"; $sResult .= $this->renderListContents($oOutputFormat); $sResult .= '}'; return $sResult; } /** * @return bool */ public function isRootList() { return false; } /** * @return string|null */ public function atRuleName() { return $this->vendorKeyFrame; } /** * @return string|null */ public function atRuleArgs() { return $this->animationName; } } packages/Sabberworm/CSS/CSSList/CSSBlockList.php 0000644 00000017013 15217646627 0015333 0 ustar 00 <?php namespace Automattic\WooCommerce\Vendor\Sabberworm\CSS\CSSList; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\CSSElement; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Property\Selector; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Rule\Rule; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\RuleSet\DeclarationBlock; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\RuleSet\RuleSet; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Value\CSSFunction; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Value\Value; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Value\ValueList; /** * A `CSSBlockList` is a `CSSList` whose `DeclarationBlock`s are guaranteed to contain valid declaration blocks or * at-rules. * * Most `CSSList`s conform to this category but some at-rules (such as `@keyframes`) do not. */ abstract class CSSBlockList extends CSSList { /** * @param int $iLineNo */ public function __construct($iLineNo = 0) { parent::__construct($iLineNo); } /** * @param array<int, DeclarationBlock> $aResult * * @return void */ protected function allDeclarationBlocks(array &$aResult) { foreach ($this->aContents as $mContent) { if ($mContent instanceof DeclarationBlock) { $aResult[] = $mContent; } elseif ($mContent instanceof CSSBlockList) { $mContent->allDeclarationBlocks($aResult); } } } /** * @param array<int, RuleSet> $aResult * * @return void */ protected function allRuleSets(array &$aResult) { foreach ($this->aContents as $mContent) { if ($mContent instanceof RuleSet) { $aResult[] = $mContent; } elseif ($mContent instanceof CSSBlockList) { $mContent->allRuleSets($aResult); } } } /** * Returns all `Value` objects found recursively in `Rule`s in the tree. * * @param CSSElement|string|null $element * This is the `CSSList` or `RuleSet` to start the search from (defaults to the whole document). * If a string is given, it is used as a rule name filter. * Passing a string for this parameter is deprecated in version 8.9.0, and will not work from v9.0; * use the following parameter to pass a rule name filter instead. * @param string|bool|null $ruleSearchPatternOrSearchInFunctionArguments * This allows filtering rules by property name * (e.g. if "color" is passed, only `Value`s from `color` properties will be returned, * or if "font-" is provided, `Value`s from all font rules, like `font-size`, and including `font` itself, * will be returned). * If a Boolean is provided, it is treated as the `$searchInFunctionArguments` argument. * Passing a Boolean for this parameter is deprecated in version 8.9.0, and will not work from v9.0; * use the `$searchInFunctionArguments` parameter instead. * @param bool $searchInFunctionArguments whether to also return Value objects used as Function arguments. * * @return array<int, Value> * * @see RuleSet->getRules() */ public function getAllValues( $element = null, $ruleSearchPatternOrSearchInFunctionArguments = null, $searchInFunctionArguments = false ) { if (\is_bool($ruleSearchPatternOrSearchInFunctionArguments)) { $searchInFunctionArguments = $ruleSearchPatternOrSearchInFunctionArguments; $searchString = null; } else { $searchString = $ruleSearchPatternOrSearchInFunctionArguments; } if ($element === null) { $element = $this; } elseif (\is_string($element)) { $searchString = $element; $element = $this; } $result = []; $this->allValues($element, $result, $searchString, $searchInFunctionArguments); return $result; } /** * @param CSSElement|string $oElement * @param array<int, Value> $aResult * @param string|null $sSearchString * @param bool $bSearchInFunctionArguments * * @return void */ protected function allValues($oElement, array &$aResult, $sSearchString = null, $bSearchInFunctionArguments = false) { if ($oElement instanceof CSSBlockList) { foreach ($oElement->getContents() as $oContent) { $this->allValues($oContent, $aResult, $sSearchString, $bSearchInFunctionArguments); } } elseif ($oElement instanceof RuleSet) { foreach ($oElement->getRules($sSearchString) as $oRule) { $this->allValues($oRule, $aResult, $sSearchString, $bSearchInFunctionArguments); } } elseif ($oElement instanceof Rule) { $this->allValues($oElement->getValue(), $aResult, $sSearchString, $bSearchInFunctionArguments); } elseif ($oElement instanceof ValueList) { if ($bSearchInFunctionArguments || !($oElement instanceof CSSFunction)) { foreach ($oElement->getListComponents() as $mComponent) { $this->allValues($mComponent, $aResult, $sSearchString, $bSearchInFunctionArguments); } } } else { // Non-List `Value` or `CSSString` (CSS identifier) $aResult[] = $oElement; } } /** * @param array<int, Selector> $aResult * @param string|null $sSpecificitySearch * * @return void */ protected function allSelectors(array &$aResult, $sSpecificitySearch = null) { /** @var array<int, DeclarationBlock> $aDeclarationBlocks */ $aDeclarationBlocks = []; $this->allDeclarationBlocks($aDeclarationBlocks); foreach ($aDeclarationBlocks as $oBlock) { foreach ($oBlock->getSelectors() as $oSelector) { if ($sSpecificitySearch === null) { $aResult[] = $oSelector; } else { $sComparator = '==='; $aSpecificitySearch = explode(' ', $sSpecificitySearch); $iTargetSpecificity = $aSpecificitySearch[0]; if (count($aSpecificitySearch) > 1) { $sComparator = $aSpecificitySearch[0]; $iTargetSpecificity = $aSpecificitySearch[1]; } $iTargetSpecificity = (int)$iTargetSpecificity; $iSelectorSpecificity = $oSelector->getSpecificity(); $bMatches = false; switch ($sComparator) { case '<=': $bMatches = $iSelectorSpecificity <= $iTargetSpecificity; break; case '<': $bMatches = $iSelectorSpecificity < $iTargetSpecificity; break; case '>=': $bMatches = $iSelectorSpecificity >= $iTargetSpecificity; break; case '>': $bMatches = $iSelectorSpecificity > $iTargetSpecificity; break; default: $bMatches = $iSelectorSpecificity === $iTargetSpecificity; break; } if ($bMatches) { $aResult[] = $oSelector; } } } } } } packages/Sabberworm/CSS/Position/Position.php 0000644 00000002663 15217646627 0015225 0 ustar 00 <?php declare(strict_types=1); namespace Automattic\WooCommerce\Vendor\Sabberworm\CSS\Position; /** * Provides a standard reusable implementation of `Positionable`. * * @internal * * @phpstan-require-implements Positionable */ trait Position { /** * @var int<1, max>|null */ protected $lineNumber; /** * @var int<0, max>|null */ protected $columnNumber; /** * @return int<1, max>|null */ public function getLineNumber() { return $this->lineNumber; } /** * @return int<0, max> */ public function getLineNo() { $lineNumber = $this->getLineNumber(); return $lineNumber !== null ? $lineNumber : 0; } /** * @return int<0, max>|null */ public function getColumnNumber() { return $this->columnNumber; } /** * @return int<0, max> */ public function getColNo() { $columnNumber = $this->getColumnNumber(); return $columnNumber !== null ? $columnNumber : 0; } /** * @param int<0, max>|null $lineNumber * @param int<0, max>|null $columnNumber */ public function setPosition($lineNumber, $columnNumber = null) { // The conditional is for backwards compatibility (backcompat); `0` will not be allowed in future. $this->lineNumber = $lineNumber !== 0 ? $lineNumber : null; $this->columnNumber = $columnNumber; } } packages/Sabberworm/CSS/Position/Positionable.php 0000644 00000002312 15217646627 0016040 0 ustar 00 <?php declare(strict_types=1); namespace Automattic\WooCommerce\Vendor\Sabberworm\CSS\Position; /** * Represents a CSS item that may have a position in the source CSS document (line number and possibly column number). * * A standard implementation of this interface is available in the `Position` trait. */ interface Positionable { /** * @return int<1, max>|null */ public function getLineNumber(); /** * @return int<0, max> * * @deprecated in version 8.9.0, will be removed in v9.0. Use `getLineNumber()` instead. */ public function getLineNo(); /** * @return int<0, max>|null */ public function getColumnNumber(); /** * @return int<0, max> * * @deprecated in version 8.9.0, will be removed in v9.0. Use `getColumnNumber()` instead. */ public function getColNo(); /** * @param int<0, max>|null $lineNumber * Providing zero for this parameter is deprecated in version 8.9.0, and will not be supported from v9.0. * Use `null` instead when no line number is available. * @param int<0, max>|null $columnNumber */ public function setPosition($lineNumber, $columnNumber = null); } packages/Sabberworm/CSS/OutputFormat.php 0000644 00000026412 15217646627 0014264 0 ustar 00 <?php namespace Automattic\WooCommerce\Vendor\Sabberworm\CSS; /** * Extending this class is deprecated in version 8.8.0; it will be made `final` in version 9.0.0. * * @method OutputFormat setSemicolonAfterLastRule(bool $bSemicolonAfterLastRule) Set whether semicolons are added after * last rule. */ class OutputFormat { /** * Value format: `"` means double-quote, `'` means single-quote * * @var string * * @internal since 8.8.0, will be made private in 9.0.0 */ public $sStringQuotingType = '"'; /** * Output RGB colors in hash notation if possible * * @var bool * * @internal since 8.8.0, will be made private in 9.0.0 */ public $bRGBHashNotation = true; /** * Declaration format * * Semicolon after the last rule of a declaration block can be omitted. To do that, set this false. * * @var bool * * @internal since 8.8.0, will be made private in 9.0.0 */ public $bSemicolonAfterLastRule = true; /** * Spacing * Note that these strings are not sanity-checked: the value should only consist of whitespace * Any newline character will be indented according to the current level. * The triples (After, Before, Between) can be set using a wildcard (e.g. `$oFormat->set('Space*Rules', "\n");`) * * @var string * * @internal since 8.8.0, will be made private in 9.0.0 */ public $sSpaceAfterRuleName = ' '; /** * @var string * * @internal since 8.8.0, will be made private in 9.0.0 */ public $sSpaceBeforeRules = ''; /** * @var string * * @internal since 8.8.0, will be made private in 9.0.0 */ public $sSpaceAfterRules = ''; /** * @var string * * @internal since 8.8.0, will be made private in 9.0.0 */ public $sSpaceBetweenRules = ''; /** * @var string * * @internal since 8.8.0, will be made private in 9.0.0 */ public $sSpaceBeforeBlocks = ''; /** * @var string * * @internal since 8.8.0, will be made private in 9.0.0 */ public $sSpaceAfterBlocks = ''; /** * @var string * * @internal since 8.8.0, will be made private in 9.0.0 */ public $sSpaceBetweenBlocks = "\n"; /** * Content injected in and around at-rule blocks. * * @var string * * @internal since 8.8.0, will be made private in 9.0.0 */ public $sBeforeAtRuleBlock = ''; /** * @var string * * @internal since 8.8.0, will be made private in 9.0.0 */ public $sAfterAtRuleBlock = ''; /** * This is what’s printed before and after the comma if a declaration block contains multiple selectors. * * @var string * * @internal since 8.8.0, will be made private in 9.0.0 */ public $sSpaceBeforeSelectorSeparator = ''; /** * @var string * * @internal since 8.8.0, will be made private in 9.0.0 */ public $sSpaceAfterSelectorSeparator = ' '; /** * This is what’s inserted before the separator in value lists, by default. * * `array` is deprecated in version 8.8.0, and will be removed in version 9.0.0. * To set the spacing for specific separators, use {@see $aSpaceBeforeListArgumentSeparators} instead. * * @var string|array<non-empty-string, string> * * @internal since 8.8.0, will be made private in 9.0.0 */ public $sSpaceBeforeListArgumentSeparator = ''; /** * Keys are separators (e.g. `,`). Values are the space sequence to insert, or an empty string. * * @var array<non-empty-string, string> * * @internal since 8.8.0, will be made private in 9.0.0 */ public $aSpaceBeforeListArgumentSeparators = []; /** * This is what’s inserted after the separator in value lists, by default. * * `array` is deprecated in version 8.8.0, and will be removed in version 9.0.0. * To set the spacing for specific separators, use {@see $aSpaceAfterListArgumentSeparators} instead. * * @var string|array<non-empty-string, string> * * @internal since 8.8.0, will be made private in 9.0.0 */ public $sSpaceAfterListArgumentSeparator = ''; /** * Keys are separators (e.g. `,`). Values are the space sequence to insert, or an empty string. * * @var array<non-empty-string, string> * * @internal since 8.8.0, will be made private in 9.0.0 */ public $aSpaceAfterListArgumentSeparators = []; /** * @var string * * @internal since 8.8.0, will be made private in 9.0.0 */ public $sSpaceBeforeOpeningBrace = ' '; /** * Content injected in and around declaration blocks. * * @var string * * @internal since 8.8.0, will be made private in 9.0.0 */ public $sBeforeDeclarationBlock = ''; /** * @var string * * @internal since 8.8.0, will be made private in 9.0.0 */ public $sAfterDeclarationBlockSelectors = ''; /** * @var string * * @internal since 8.8.0, will be made private in 9.0.0 */ public $sAfterDeclarationBlock = ''; /** * Indentation character(s) per level. Only applicable if newlines are used in any of the spacing settings. * * @var string * * @internal since 8.8.0, will be made private in 9.0.0 */ public $sIndentation = "\t"; /** * Output exceptions. * * @var bool * * @internal since 8.8.0, will be made private in 9.0.0 */ public $bIgnoreExceptions = false; /** * Render comments for lists and RuleSets * * @var bool * * @internal since 8.8.0, will be made private in 9.0.0 */ public $bRenderComments = false; /** * @var OutputFormatter|null */ private $oFormatter = null; /** * @var OutputFormat|null */ private $oNextLevelFormat = null; /** * @var int */ private $iIndentationLevel = 0; /** * @internal since V8.8.0. Use the factory methods `create()`, `createCompact()`, or `createPretty()` instead. */ public function __construct() { } /** * @param string $sName * * @return string|null * * @deprecated since 8.8.0, will be removed in 9.0.0. Use specific getters instead. */ public function get($sName) { $aVarPrefixes = ['a', 's', 'm', 'b', 'f', 'o', 'c', 'i']; foreach ($aVarPrefixes as $sPrefix) { $sFieldName = $sPrefix . ucfirst($sName); if (isset($this->$sFieldName)) { return $this->$sFieldName; } } return null; } /** * @param array<array-key, string>|string $aNames * @param mixed $mValue * * @return self|false * * @deprecated since 8.8.0, will be removed in 9.0.0. Use specific setters instead. */ public function set($aNames, $mValue) { $aVarPrefixes = ['a', 's', 'm', 'b', 'f', 'o', 'c', 'i']; if (is_string($aNames) && strpos($aNames, '*') !== false) { $aNames = [ str_replace('*', 'Before', $aNames), str_replace('*', 'Between', $aNames), str_replace('*', 'After', $aNames), ]; } elseif (!is_array($aNames)) { $aNames = [$aNames]; } foreach ($aVarPrefixes as $sPrefix) { $bDidReplace = false; foreach ($aNames as $sName) { $sFieldName = $sPrefix . ucfirst($sName); if (isset($this->$sFieldName)) { $this->$sFieldName = $mValue; $bDidReplace = true; } } if ($bDidReplace) { return $this; } } // Break the chain so the user knows this option is invalid return false; } /** * @param string $sMethodName * @param array<array-key, mixed> $aArguments * * @return mixed * * @throws \Exception */ public function __call($sMethodName, array $aArguments) { if (strpos($sMethodName, 'set') === 0) { return $this->set(substr($sMethodName, 3), $aArguments[0]); } elseif (strpos($sMethodName, 'get') === 0) { return $this->get(substr($sMethodName, 3)); } elseif (method_exists(OutputFormatter::class, $sMethodName)) { // @deprecated since 8.8.0, will be removed in 9.0.0. Call the method on the formatter directly instead. return call_user_func_array([$this->getFormatter(), $sMethodName], $aArguments); } else { throw new \Exception('Unknown OutputFormat method called: ' . $sMethodName); } } /** * @param int $iNumber * * @return self */ public function indentWithTabs($iNumber = 1) { return $this->setIndentation(str_repeat("\t", $iNumber)); } /** * @param int $iNumber * * @return self */ public function indentWithSpaces($iNumber = 2) { return $this->setIndentation(str_repeat(" ", $iNumber)); } /** * @return OutputFormat * * @internal since V8.8.0 */ public function nextLevel() { if ($this->oNextLevelFormat === null) { $this->oNextLevelFormat = clone $this; $this->oNextLevelFormat->iIndentationLevel++; $this->oNextLevelFormat->oFormatter = null; } return $this->oNextLevelFormat; } /** * @return void */ public function beLenient() { $this->bIgnoreExceptions = true; } /** * @return OutputFormatter * * @internal since 8.8.0 */ public function getFormatter() { if ($this->oFormatter === null) { $this->oFormatter = new OutputFormatter($this); } return $this->oFormatter; } /** * @return int * * @deprecated #869 since version V8.8.0, will be removed in V9.0.0. Use `getIndentationLevel()` instead. */ public function level() { return $this->iIndentationLevel; } /** * Creates an instance of this class without any particular formatting settings. * * @return self */ public static function create() { return new OutputFormat(); } /** * Creates an instance of this class with a preset for compact formatting. * * @return self */ public static function createCompact() { $format = self::create(); $format->set('Space*Rules', "") ->set('Space*Blocks', "") ->setSpaceAfterRuleName('') ->setSpaceBeforeOpeningBrace('') ->setSpaceAfterSelectorSeparator('') ->setRenderComments(false); return $format; } /** * Creates an instance of this class with a preset for pretty formatting. * * @return self */ public static function createPretty() { $format = self::create(); $format->set('Space*Rules', "\n") ->set('Space*Blocks', "\n") ->setSpaceBetweenBlocks("\n\n") ->set('SpaceAfterListArgumentSeparators', [',' => ' ']) ->setRenderComments(true); return $format; } } packages/Sabberworm/CSS/OutputFormatter.php 0000644 00000014625 15217646627 0015002 0 ustar 00 <?php namespace Automattic\WooCommerce\Vendor\Sabberworm\CSS; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Comment\Commentable; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Parsing\OutputException; /** * @internal since 8.8.0 */ class OutputFormatter { /** * @var OutputFormat */ private $oFormat; public function __construct(OutputFormat $oFormat) { $this->oFormat = $oFormat; } /** * @param string $sName * @param string|null $sType * * @return string */ public function space($sName, $sType = null) { $sSpaceString = $this->oFormat->get("Space$sName"); // If $sSpaceString is an array, we have multiple values configured // depending on the type of object the space applies to if (is_array($sSpaceString)) { if ($sType !== null && isset($sSpaceString[$sType])) { $sSpaceString = $sSpaceString[$sType]; } else { $sSpaceString = reset($sSpaceString); } } return $this->prepareSpace($sSpaceString); } /** * @return string */ public function spaceAfterRuleName() { return $this->space('AfterRuleName'); } /** * @return string */ public function spaceBeforeRules() { return $this->space('BeforeRules'); } /** * @return string */ public function spaceAfterRules() { return $this->space('AfterRules'); } /** * @return string */ public function spaceBetweenRules() { return $this->space('BetweenRules'); } /** * @return string */ public function spaceBeforeBlocks() { return $this->space('BeforeBlocks'); } /** * @return string */ public function spaceAfterBlocks() { return $this->space('AfterBlocks'); } /** * @return string */ public function spaceBetweenBlocks() { return $this->space('BetweenBlocks'); } /** * @return string */ public function spaceBeforeSelectorSeparator() { return $this->space('BeforeSelectorSeparator'); } /** * @return string */ public function spaceAfterSelectorSeparator() { return $this->space('AfterSelectorSeparator'); } /** * @param string $sSeparator * * @return string */ public function spaceBeforeListArgumentSeparator($sSeparator) { $spaceForSeparator = $this->oFormat->getSpaceBeforeListArgumentSeparators(); if (isset($spaceForSeparator[$sSeparator])) { return $spaceForSeparator[$sSeparator]; } return $this->space('BeforeListArgumentSeparator', $sSeparator); } /** * @param string $sSeparator * * @return string */ public function spaceAfterListArgumentSeparator($sSeparator) { $spaceForSeparator = $this->oFormat->getSpaceAfterListArgumentSeparators(); if (isset($spaceForSeparator[$sSeparator])) { return $spaceForSeparator[$sSeparator]; } return $this->space('AfterListArgumentSeparator', $sSeparator); } /** * @return string */ public function spaceBeforeOpeningBrace() { return $this->space('BeforeOpeningBrace'); } /** * Runs the given code, either swallowing or passing exceptions, depending on the `bIgnoreExceptions` setting. * * @param string $cCode the name of the function to call * * @return string|null */ public function safely($cCode) { if ($this->oFormat->get('IgnoreExceptions')) { // If output exceptions are ignored, run the code with exception guards try { return $cCode(); } catch (OutputException $e) { return null; } // Do nothing } else { // Run the code as-is return $cCode(); } } /** * Clone of the `implode` function, but calls `render` with the current output format instead of `__toString()`. * * @param string $sSeparator * @param array<array-key, Renderable|string> $aValues * @param bool $bIncreaseLevel * * @return string */ public function implode($sSeparator, array $aValues, $bIncreaseLevel = false) { $sResult = ''; $oFormat = $this->oFormat; if ($bIncreaseLevel) { $oFormat = $oFormat->nextLevel(); } $bIsFirst = true; foreach ($aValues as $mValue) { if ($bIsFirst) { $bIsFirst = false; } else { $sResult .= $sSeparator; } if ($mValue instanceof Renderable) { $sResult .= $mValue->render($oFormat); } else { $sResult .= $mValue; } } return $sResult; } /** * @param string $sString * * @return string */ public function removeLastSemicolon($sString) { if ($this->oFormat->get('SemicolonAfterLastRule')) { return $sString; } $sString = explode(';', $sString); if (count($sString) < 2) { return $sString[0]; } $sLast = array_pop($sString); $sNextToLast = array_pop($sString); array_push($sString, $sNextToLast . $sLast); return implode(';', $sString); } /** * * @param array<Commentable> $aComments * * @return string */ public function comments(Commentable $oCommentable) { if (!$this->oFormat->bRenderComments) { return ''; } $sResult = ''; $aComments = $oCommentable->getComments(); $iLastCommentIndex = count($aComments) - 1; foreach ($aComments as $i => $oComment) { $sResult .= $oComment->render($this->oFormat); $sResult .= $i === $iLastCommentIndex ? $this->spaceAfterBlocks() : $this->spaceBetweenBlocks(); } return $sResult; } /** * @param string $sSpaceString * * @return string */ private function prepareSpace($sSpaceString) { return str_replace("\n", "\n" . $this->indent(), $sSpaceString); } /** * @return string */ private function indent() { return str_repeat($this->oFormat->sIndentation, $this->oFormat->getIndentationLevel()); } } packages/Sabberworm/CSS/CSSElement.php 0000644 00000001050 15217646627 0013544 0 ustar 00 <?php declare(strict_types=1); namespace Automattic\WooCommerce\Vendor\Sabberworm\CSS; /** * Represents any entity in the CSS that is encapsulated by a class. * * Its primary purpose is to provide a type for use with `Document::getAllValues()` * when a subset of values from a particular part of the document is required. * * Thus, elements which don't contain `Value`s (such as statement at-rules) don't need to implement this. * * It extends `Renderable` because every element is renderable. */ interface CSSElement extends Renderable {} packages/Sabberworm/CSS/RuleSet/AtRuleSet.php 0000644 00000003376 15217646630 0015044 0 ustar 00 <?php namespace Automattic\WooCommerce\Vendor\Sabberworm\CSS\RuleSet; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\OutputFormat; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Property\AtRule; /** * This class represents rule sets for generic at-rules which are not covered by specific classes, i.e., not * `@import`, `@charset` or `@media`. * * A common example for this is `@font-face`. */ class AtRuleSet extends RuleSet implements AtRule { /** * @var string */ private $sType; /** * @var string */ private $sArgs; /** * @param string $sType * @param string $sArgs * @param int $iLineNo */ public function __construct($sType, $sArgs = '', $iLineNo = 0) { parent::__construct($iLineNo); $this->sType = $sType; $this->sArgs = $sArgs; } /** * @return string */ public function atRuleName() { return $this->sType; } /** * @return string */ public function atRuleArgs() { return $this->sArgs; } /** * @return string * * @deprecated in V8.8.0, will be removed in V9.0.0. Use `render` instead. */ public function __toString() { return $this->render(new OutputFormat()); } /** * @param OutputFormat|null $oOutputFormat * * @return string */ public function render($oOutputFormat) { $sResult = $oOutputFormat->comments($this); $sArgs = $this->sArgs; if ($sArgs) { $sArgs = ' ' . $sArgs; } $sResult .= "@{$this->sType}$sArgs{$oOutputFormat->spaceBeforeOpeningBrace()}{"; $sResult .= $this->renderRules($oOutputFormat); $sResult .= '}'; return $sResult; } } packages/Sabberworm/CSS/RuleSet/RuleSet.php 0000644 00000030310 15217646630 0014543 0 ustar 00 <?php namespace Automattic\WooCommerce\Vendor\Sabberworm\CSS\RuleSet; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Comment\Comment; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Comment\Commentable; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\CSSElement; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\OutputFormat; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Parsing\ParserState; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Parsing\UnexpectedEOFException; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Parsing\UnexpectedTokenException; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Position\Position; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Position\Positionable; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Renderable; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Rule\Rule; /** * This class is a container for individual 'Rule's. * * The most common form of a rule set is one constrained by a selector, i.e., a `DeclarationBlock`. * However, unknown `AtRule`s (like `@font-face`) are rule sets as well. * * If you want to manipulate a `RuleSet`, use the methods `addRule(Rule $rule)`, `getRules()` and `removeRule($rule)` * (which accepts either a `Rule` or a rule name; optionally suffixed by a dash to remove all related rules). */ abstract class RuleSet implements CSSElement, Commentable, Positionable { use Position; /** * the rules in this rule set, using the property name as the key, * with potentially multiple rules per property name. * * @var array<string, array<int<0, max>, Rule>> */ private $aRules; /** * @var array<array-key, Comment> * * @internal since 8.8.0 */ protected $aComments; /** * @param int $iLineNo */ public function __construct($iLineNo = 0) { $this->aRules = []; $this->setPosition($iLineNo); $this->aComments = []; } /** * @return void * * @throws UnexpectedTokenException * @throws UnexpectedEOFException * * @internal since V8.8.0 */ public static function parseRuleSet(ParserState $oParserState, RuleSet $oRuleSet) { while ($oParserState->comes(';')) { $oParserState->consume(';'); } while (true) { $commentsBeforeRule = $oParserState->consumeWhiteSpace(); if ($oParserState->comes('}')) { break; } $oRule = null; if ($oParserState->getSettings()->bLenientParsing) { try { $oRule = Rule::parse($oParserState, $commentsBeforeRule); } catch (UnexpectedTokenException $e) { try { $sConsume = $oParserState->consumeUntil(["\n", ";", '}'], true); // We need to “unfind” the matches to the end of the ruleSet as this will be matched later if ($oParserState->streql(substr($sConsume, -1), '}')) { $oParserState->backtrack(1); } else { while ($oParserState->comes(';')) { $oParserState->consume(';'); } } } catch (UnexpectedTokenException $e) { // We’ve reached the end of the document. Just close the RuleSet. return; } } } else { $oRule = Rule::parse($oParserState, $commentsBeforeRule); } if ($oRule) { $oRuleSet->addRule($oRule); } } $oParserState->consume('}'); } /** * @param Rule|null $oSibling * * @return void */ public function addRule(Rule $oRule, $oSibling = null) { $sRule = $oRule->getRule(); if (!isset($this->aRules[$sRule])) { $this->aRules[$sRule] = []; } $iPosition = count($this->aRules[$sRule]); if ($oSibling !== null) { $iSiblingPos = array_search($oSibling, $this->aRules[$sRule], true); if ($iSiblingPos !== false) { $iPosition = $iSiblingPos; $oRule->setPosition($oSibling->getLineNo(), $oSibling->getColNo() - 1); } } if ($oRule->getLineNumber() === null) { //this node is added manually, give it the next best line $columnNumber = $oRule->getColNo(); $rules = $this->getRules(); $pos = count($rules); if ($pos > 0) { $last = $rules[$pos - 1]; $oRule->setPosition($last->getLineNo() + 1, $columnNumber); } else { $oRule->setPosition(1, $columnNumber); } } elseif ($oRule->getColumnNumber() === null) { $oRule->setPosition($oRule->getLineNumber(), 0); } array_splice($this->aRules[$sRule], $iPosition, 0, [$oRule]); } /** * Returns all rules matching the given rule name * * @example $oRuleSet->getRules('font') // returns array(0 => $oRule, …) or array(). * * @example $oRuleSet->getRules('font-') * //returns an array of all rules either beginning with font- or matching font. * * @param Rule|string|null $mRule * Pattern to search for. If null, returns all rules. * If the pattern ends with a dash, all rules starting with the pattern are returned * as well as one matching the pattern with the dash excluded. * Passing a `Rule` for this parameter is deprecated in version 8.9.0, and will not work from v9.0. * Call `getRules($rule->getRule())` instead. * * @return array<int, Rule> */ public function getRules($mRule = null) { if ($mRule instanceof Rule) { $mRule = $mRule->getRule(); } /** @var array<int, Rule> $aResult */ $aResult = []; foreach ($this->aRules as $sName => $aRules) { // Either no search rule is given or the search rule matches the found rule exactly // or the search rule ends in “-” and the found rule starts with the search rule. if ( !$mRule || $sName === $mRule || ( strrpos($mRule, '-') === strlen($mRule) - strlen('-') && (strpos($sName, $mRule) === 0 || $sName === substr($mRule, 0, -1)) ) ) { $aResult = array_merge($aResult, $aRules); } } usort($aResult, function (Rule $first, Rule $second) { if ($first->getLineNo() === $second->getLineNo()) { return $first->getColNo() - $second->getColNo(); } return $first->getLineNo() - $second->getLineNo(); }); return $aResult; } /** * Overrides all the rules of this set. * * @param array<array-key, Rule> $aRules The rules to override with. * * @return void */ public function setRules(array $aRules) { $this->aRules = []; foreach ($aRules as $rule) { $this->addRule($rule); } } /** * Returns all rules matching the given pattern and returns them in an associative array with the rule’s name * as keys. This method exists mainly for backwards-compatibility and is really only partially useful. * * Note: This method loses some information: Calling this (with an argument of `background-`) on a declaration block * like `{ background-color: green; background-color; rgba(0, 127, 0, 0.7); }` will only yield an associative array * containing the rgba-valued rule while `getRules()` would yield an indexed array containing both. * * @param Rule|string|null $mRule $mRule * Pattern to search for. If null, returns all rules. If the pattern ends with a dash, * all rules starting with the pattern are returned as well as one matching the pattern with the dash * excluded. * Passing a `Rule` for this parameter is deprecated in version 8.9.0, and will not work from v9.0. * Call `getRulesAssoc($rule->getRule())` instead. * * @return array<string, Rule> */ public function getRulesAssoc($mRule = null) { /** @var array<string, Rule> $aResult */ $aResult = []; foreach ($this->getRules($mRule) as $oRule) { $aResult[$oRule->getRule()] = $oRule; } return $aResult; } /** * Removes a `Rule` from this `RuleSet` by identity. * * @param Rule|string|null $mRule * `Rule` to remove. * Passing a `string` or `null` is deprecated in version 8.9.0, and will no longer work from v9.0. * Use `removeMatchingRules()` or `removeAllRules()` instead. */ public function removeRule($mRule) { if ($mRule instanceof Rule) { $sRule = $mRule->getRule(); if (!isset($this->aRules[$sRule])) { return; } foreach ($this->aRules[$sRule] as $iKey => $oRule) { if ($oRule === $mRule) { unset($this->aRules[$sRule][$iKey]); } } } elseif ($mRule !== null) { $this->removeMatchingRules($mRule); } else { $this->removeAllRules(); } } /** * Removes rules by property name or search pattern. * * @param string $searchPattern * pattern to remove. * If the pattern ends in a dash, * all rules starting with the pattern are removed as well as one matching the pattern with the dash * excluded. */ public function removeMatchingRules($searchPattern) { foreach ($this->aRules as $propertyName => $rules) { // Either the search rule matches the found rule exactly // or the search rule ends in “-” and the found rule starts with the search rule or equals it // (without the trailing dash). if ( $propertyName === $searchPattern || (\strrpos($searchPattern, '-') === \strlen($searchPattern) - \strlen('-') && (\strpos($propertyName, $searchPattern) === 0 || $propertyName === \substr($searchPattern, 0, -1))) ) { unset($this->aRules[$propertyName]); } } } public function removeAllRules() { $this->aRules = []; } /** * @return string * * @deprecated in V8.8.0, will be removed in V9.0.0. Use `render` instead. */ public function __toString() { return $this->render(new OutputFormat()); } /** * @return string */ protected function renderRules(OutputFormat $oOutputFormat) { $sResult = ''; $bIsFirst = true; $oNextLevel = $oOutputFormat->nextLevel(); foreach ($this->getRules() as $oRule) { $sRendered = $oNextLevel->safely(function () use ($oRule, $oNextLevel) { return $oRule->render($oNextLevel); }); if ($sRendered === null) { continue; } if ($bIsFirst) { $bIsFirst = false; $sResult .= $oNextLevel->spaceBeforeRules(); } else { $sResult .= $oNextLevel->spaceBetweenRules(); } $sResult .= $sRendered; } if (!$bIsFirst) { // Had some output $sResult .= $oOutputFormat->spaceAfterRules(); } return $oOutputFormat->removeLastSemicolon($sResult); } /** * @param array<string, Comment> $aComments * * @return void */ public function addComments(array $aComments) { $this->aComments = array_merge($this->aComments, $aComments); } /** * @return array<string, Comment> */ public function getComments() { return $this->aComments; } /** * @param array<string, Comment> $aComments * * @return void */ public function setComments(array $aComments) { $this->aComments = $aComments; } } packages/Sabberworm/CSS/RuleSet/DeclarationBlock.php 0000644 00000076041 15217646630 0016373 0 ustar 00 <?php namespace Automattic\WooCommerce\Vendor\Sabberworm\CSS\RuleSet; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\CSSList\CSSList; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\CSSList\KeyFrame; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\OutputFormat; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Parsing\OutputException; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Parsing\ParserState; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Parsing\UnexpectedEOFException; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Parsing\UnexpectedTokenException; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Property\KeyframeSelector; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Property\Selector; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Rule\Rule; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Value\Color; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Value\RuleValueList; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Value\Size; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Value\URL; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Value\Value; /** * This class represents a `RuleSet` constrained by a `Selector`. * * It contains an array of selector objects (comma-separated in the CSS) as well as the rules to be applied to the * matching elements. * * Declaration blocks usually appear directly inside a `Document` or another `CSSList` (mostly a `MediaQuery`). */ class DeclarationBlock extends RuleSet { /** * @var array<int, Selector|string> */ private $aSelectors; /** * @param int $iLineNo */ public function __construct($iLineNo = 0) { parent::__construct($iLineNo); $this->aSelectors = []; } /** * @param CSSList|null $oList * * @return DeclarationBlock|false * * @throws UnexpectedTokenException * @throws UnexpectedEOFException * * @internal since V8.8.0 */ public static function parse(ParserState $oParserState, $oList = null) { $aComments = []; $oResult = new DeclarationBlock($oParserState->currentLine()); try { $aSelectorParts = []; $sStringWrapperChar = false; do { $aSelectorParts[] = $oParserState->consume(1) . $oParserState->consumeUntil(['{', '}', '\'', '"'], false, false, $aComments); if (in_array($oParserState->peek(), ['\'', '"']) && substr(end($aSelectorParts), -1) != "\\") { if ($sStringWrapperChar === false) { $sStringWrapperChar = $oParserState->peek(); } elseif ($sStringWrapperChar == $oParserState->peek()) { $sStringWrapperChar = false; } } } while (!in_array($oParserState->peek(), ['{', '}']) || $sStringWrapperChar !== false); $oResult->setSelectors(implode('', $aSelectorParts), $oList); if ($oParserState->comes('{')) { $oParserState->consume(1); } } catch (UnexpectedTokenException $e) { if ($oParserState->getSettings()->bLenientParsing) { if (!$oParserState->comes('}')) { $oParserState->consumeUntil('}', false, true); } return false; } else { throw $e; } } $oResult->setComments($aComments); RuleSet::parseRuleSet($oParserState, $oResult); return $oResult; } /** * @param array<int, Selector|string>|string $mSelector * @param CSSList|null $oList * * @throws UnexpectedTokenException */ public function setSelectors($mSelector, $oList = null) { if (is_array($mSelector)) { $this->aSelectors = $mSelector; } else { $this->aSelectors = explode(',', $mSelector); } foreach ($this->aSelectors as $iKey => $mSelector) { if (!($mSelector instanceof Selector)) { if ($oList === null || !($oList instanceof KeyFrame)) { if (!Selector::isValid($mSelector)) { throw new UnexpectedTokenException( "Selector did not match '" . Selector::SELECTOR_VALIDATION_RX . "'.", $mSelector, "custom" ); } $this->aSelectors[$iKey] = new Selector($mSelector); } else { if (!KeyframeSelector::isValid($mSelector)) { throw new UnexpectedTokenException( "Selector did not match '" . KeyframeSelector::SELECTOR_VALIDATION_RX . "'.", $mSelector, "custom" ); } $this->aSelectors[$iKey] = new KeyframeSelector($mSelector); } } } } /** * Remove one of the selectors of the block. * * @param Selector|string $mSelector * * @return bool */ public function removeSelector($mSelector) { if ($mSelector instanceof Selector) { $mSelector = $mSelector->getSelector(); } foreach ($this->aSelectors as $iKey => $oSelector) { if ($oSelector->getSelector() === $mSelector) { unset($this->aSelectors[$iKey]); return true; } } return false; } /** * @return array<int, Selector|string> * * @deprecated will be removed in version 9.0; use `getSelectors()` instead */ public function getSelector() { return $this->getSelectors(); } /** * @param Selector|string $mSelector * @param CSSList|null $oList * * @return void * * @deprecated will be removed in version 9.0; use `setSelectors()` instead */ public function setSelector($mSelector, $oList = null) { $this->setSelectors($mSelector, $oList); } /** * @return array<int, Selector|string> */ public function getSelectors() { return $this->aSelectors; } /** * Splits shorthand declarations (e.g. `margin` or `font`) into their constituent parts. * * @return void * * @deprecated since 8.7.0, will be removed without substitution in version 9.0 in #511 */ public function expandShorthands() { // border must be expanded before dimensions $this->expandBorderShorthand(); $this->expandDimensionsShorthand(); $this->expandFontShorthand(); $this->expandBackgroundShorthand(); $this->expandListStyleShorthand(); } /** * Creates shorthand declarations (e.g. `margin` or `font`) whenever possible. * * @return void * * @deprecated since 8.7.0, will be removed without substitution in version 9.0 in #511 */ public function createShorthands() { $this->createBackgroundShorthand(); $this->createDimensionsShorthand(); // border must be shortened after dimensions $this->createBorderShorthand(); $this->createFontShorthand(); $this->createListStyleShorthand(); } /** * Splits shorthand border declarations (e.g. `border: 1px red;`). * * Additional splitting happens in expandDimensionsShorthand. * * Multiple borders are not yet supported as of 3. * * @return void * * @deprecated since 8.7.0, will be removed without substitution in version 9.0 in #511 */ public function expandBorderShorthand() { $aBorderRules = [ 'border', 'border-left', 'border-right', 'border-top', 'border-bottom', ]; $aBorderSizes = [ 'thin', 'medium', 'thick', ]; $aRules = $this->getRulesAssoc(); foreach ($aBorderRules as $sBorderRule) { if (!isset($aRules[$sBorderRule])) { continue; } $oRule = $aRules[$sBorderRule]; $mRuleValue = $oRule->getValue(); $aValues = []; if (!$mRuleValue instanceof RuleValueList) { $aValues[] = $mRuleValue; } else { $aValues = $mRuleValue->getListComponents(); } foreach ($aValues as $mValue) { if ($mValue instanceof Value) { $mNewValue = clone $mValue; } else { $mNewValue = $mValue; } if ($mValue instanceof Size) { $sNewRuleName = $sBorderRule . "-width"; } elseif ($mValue instanceof Color) { $sNewRuleName = $sBorderRule . "-color"; } else { if (in_array($mValue, $aBorderSizes)) { $sNewRuleName = $sBorderRule . "-width"; } else { $sNewRuleName = $sBorderRule . "-style"; } } $oNewRule = new Rule($sNewRuleName, $oRule->getLineNo(), $oRule->getColNo()); $oNewRule->setIsImportant($oRule->getIsImportant()); $oNewRule->addValue([$mNewValue]); $this->addRule($oNewRule); } $this->removeRule($sBorderRule); } } /** * Splits shorthand dimensional declarations (e.g. `margin: 0px auto;`) * into their constituent parts. * * Handles `margin`, `padding`, `border-color`, `border-style` and `border-width`. * * @return void * * @deprecated since 8.7.0, will be removed without substitution in version 9.0 in #511 */ public function expandDimensionsShorthand() { $aExpansions = [ 'margin' => 'margin-%s', 'padding' => 'padding-%s', 'border-color' => 'border-%s-color', 'border-style' => 'border-%s-style', 'border-width' => 'border-%s-width', ]; $aRules = $this->getRulesAssoc(); foreach ($aExpansions as $sProperty => $sExpanded) { if (!isset($aRules[$sProperty])) { continue; } $oRule = $aRules[$sProperty]; $mRuleValue = $oRule->getValue(); $aValues = []; if (!$mRuleValue instanceof RuleValueList) { $aValues[] = $mRuleValue; } else { $aValues = $mRuleValue->getListComponents(); } $top = $right = $bottom = $left = null; switch (count($aValues)) { case 1: $top = $right = $bottom = $left = $aValues[0]; break; case 2: $top = $bottom = $aValues[0]; $left = $right = $aValues[1]; break; case 3: $top = $aValues[0]; $left = $right = $aValues[1]; $bottom = $aValues[2]; break; case 4: $top = $aValues[0]; $right = $aValues[1]; $bottom = $aValues[2]; $left = $aValues[3]; break; } foreach (['top', 'right', 'bottom', 'left'] as $sPosition) { $oNewRule = new Rule(sprintf($sExpanded, $sPosition), $oRule->getLineNo(), $oRule->getColNo()); $oNewRule->setIsImportant($oRule->getIsImportant()); $oNewRule->addValue(${$sPosition}); $this->addRule($oNewRule); } $this->removeRule($sProperty); } } /** * Converts shorthand font declarations * (e.g. `font: 300 italic 11px/14px verdana, helvetica, sans-serif;`) * into their constituent parts. * * @return void * * @deprecated since 8.7.0, will be removed without substitution in version 9.0 in #511 */ public function expandFontShorthand() { $aRules = $this->getRulesAssoc(); if (!isset($aRules['font'])) { return; } $oRule = $aRules['font']; // reset properties to 'normal' per http://www.w3.org/TR/21/fonts.html#font-shorthand $aFontProperties = [ 'font-style' => 'normal', 'font-variant' => 'normal', 'font-weight' => 'normal', 'font-size' => 'normal', 'line-height' => 'normal', ]; $mRuleValue = $oRule->getValue(); $aValues = []; if (!$mRuleValue instanceof RuleValueList) { $aValues[] = $mRuleValue; } else { $aValues = $mRuleValue->getListComponents(); } foreach ($aValues as $mValue) { if (!$mValue instanceof Value) { $mValue = mb_strtolower($mValue); } if (in_array($mValue, ['normal', 'inherit'])) { foreach (['font-style', 'font-weight', 'font-variant'] as $sProperty) { if (!isset($aFontProperties[$sProperty])) { $aFontProperties[$sProperty] = $mValue; } } } elseif (in_array($mValue, ['italic', 'oblique'])) { $aFontProperties['font-style'] = $mValue; } elseif ($mValue == 'small-caps') { $aFontProperties['font-variant'] = $mValue; } elseif ( in_array($mValue, ['bold', 'bolder', 'lighter']) || ($mValue instanceof Size && in_array($mValue->getSize(), range(100, 900, 100))) ) { $aFontProperties['font-weight'] = $mValue; } elseif ($mValue instanceof RuleValueList && $mValue->getListSeparator() == '/') { list($oSize, $oHeight) = $mValue->getListComponents(); $aFontProperties['font-size'] = $oSize; $aFontProperties['line-height'] = $oHeight; } elseif ($mValue instanceof Size && $mValue->getUnit() !== null) { $aFontProperties['font-size'] = $mValue; } else { $aFontProperties['font-family'] = $mValue; } } foreach ($aFontProperties as $sProperty => $mValue) { $oNewRule = new Rule($sProperty, $oRule->getLineNo(), $oRule->getColNo()); $oNewRule->addValue($mValue); $oNewRule->setIsImportant($oRule->getIsImportant()); $this->addRule($oNewRule); } $this->removeRule('font'); } /** * Converts shorthand background declarations * (e.g. `background: url("chess.png") gray 50% repeat fixed;`) * into their constituent parts. * * @see http://www.w3.org/TR/21/colors.html#propdef-background * * @return void * * @deprecated since 8.7.0, will be removed without substitution in version 9.0 in #511 */ public function expandBackgroundShorthand() { $aRules = $this->getRulesAssoc(); if (!isset($aRules['background'])) { return; } $oRule = $aRules['background']; $aBgProperties = [ 'background-color' => ['transparent'], 'background-image' => ['none'], 'background-repeat' => ['repeat'], 'background-attachment' => ['scroll'], 'background-position' => [ new Size(0, '%', false, $this->getLineNo()), new Size(0, '%', false, $this->getLineNo()), ], ]; $mRuleValue = $oRule->getValue(); $aValues = []; if (!$mRuleValue instanceof RuleValueList) { $aValues[] = $mRuleValue; } else { $aValues = $mRuleValue->getListComponents(); } if (count($aValues) == 1 && $aValues[0] == 'inherit') { foreach ($aBgProperties as $sProperty => $mValue) { $oNewRule = new Rule($sProperty, $oRule->getLineNo(), $oRule->getColNo()); $oNewRule->addValue('inherit'); $oNewRule->setIsImportant($oRule->getIsImportant()); $this->addRule($oNewRule); } $this->removeRule('background'); return; } $iNumBgPos = 0; foreach ($aValues as $mValue) { if (!$mValue instanceof Value) { $mValue = mb_strtolower($mValue); } if ($mValue instanceof URL) { $aBgProperties['background-image'] = $mValue; } elseif ($mValue instanceof Color) { $aBgProperties['background-color'] = $mValue; } elseif (in_array($mValue, ['scroll', 'fixed'])) { $aBgProperties['background-attachment'] = $mValue; } elseif (in_array($mValue, ['repeat', 'no-repeat', 'repeat-x', 'repeat-y'])) { $aBgProperties['background-repeat'] = $mValue; } elseif ( in_array($mValue, ['left', 'center', 'right', 'top', 'bottom']) || $mValue instanceof Size ) { if ($iNumBgPos == 0) { $aBgProperties['background-position'][0] = $mValue; $aBgProperties['background-position'][1] = 'center'; } else { $aBgProperties['background-position'][$iNumBgPos] = $mValue; } $iNumBgPos++; } } foreach ($aBgProperties as $sProperty => $mValue) { $oNewRule = new Rule($sProperty, $oRule->getLineNo(), $oRule->getColNo()); $oNewRule->setIsImportant($oRule->getIsImportant()); $oNewRule->addValue($mValue); $this->addRule($oNewRule); } $this->removeRule('background'); } /** * @return void * * @deprecated since 8.7.0, will be removed without substitution in version 9.0 in #511 */ public function expandListStyleShorthand() { $aListProperties = [ 'list-style-type' => 'disc', 'list-style-position' => 'outside', 'list-style-image' => 'none', ]; $aListStyleTypes = [ 'none', 'disc', 'circle', 'square', 'decimal-leading-zero', 'decimal', 'lower-roman', 'upper-roman', 'lower-greek', 'lower-alpha', 'lower-latin', 'upper-alpha', 'upper-latin', 'hebrew', 'armenian', 'georgian', 'cjk-ideographic', 'hiragana', 'hira-gana-iroha', 'katakana-iroha', 'katakana', ]; $aListStylePositions = [ 'inside', 'outside', ]; $aRules = $this->getRulesAssoc(); if (!isset($aRules['list-style'])) { return; } $oRule = $aRules['list-style']; $mRuleValue = $oRule->getValue(); $aValues = []; if (!$mRuleValue instanceof RuleValueList) { $aValues[] = $mRuleValue; } else { $aValues = $mRuleValue->getListComponents(); } if (count($aValues) == 1 && $aValues[0] == 'inherit') { foreach ($aListProperties as $sProperty => $mValue) { $oNewRule = new Rule($sProperty, $oRule->getLineNo(), $oRule->getColNo()); $oNewRule->addValue('inherit'); $oNewRule->setIsImportant($oRule->getIsImportant()); $this->addRule($oNewRule); } $this->removeRule('list-style'); return; } foreach ($aValues as $mValue) { if (!$mValue instanceof Value) { $mValue = mb_strtolower($mValue); } if ($mValue instanceof Url) { $aListProperties['list-style-image'] = $mValue; } elseif (in_array($mValue, $aListStyleTypes)) { $aListProperties['list-style-types'] = $mValue; } elseif (in_array($mValue, $aListStylePositions)) { $aListProperties['list-style-position'] = $mValue; } } foreach ($aListProperties as $sProperty => $mValue) { $oNewRule = new Rule($sProperty, $oRule->getLineNo(), $oRule->getColNo()); $oNewRule->setIsImportant($oRule->getIsImportant()); $oNewRule->addValue($mValue); $this->addRule($oNewRule); } $this->removeRule('list-style'); } /** * @param array<array-key, string> $aProperties * @param string $sShorthand * * @return void * * @deprecated since 8.7.0, will be removed without substitution in version 9.0 in #511 */ public function createShorthandProperties(array $aProperties, $sShorthand) { $aRules = $this->getRulesAssoc(); $oRule = null; $aNewValues = []; foreach ($aProperties as $sProperty) { if (!isset($aRules[$sProperty])) { continue; } $oRule = $aRules[$sProperty]; if (!$oRule->getIsImportant()) { $mRuleValue = $oRule->getValue(); $aValues = []; if (!$mRuleValue instanceof RuleValueList) { $aValues[] = $mRuleValue; } else { $aValues = $mRuleValue->getListComponents(); } foreach ($aValues as $mValue) { $aNewValues[] = $mValue; } $this->removeRule($sProperty); } } if ($aNewValues !== [] && $oRule instanceof Rule) { $oNewRule = new Rule($sShorthand, $oRule->getLineNo(), $oRule->getColNo()); foreach ($aNewValues as $mValue) { $oNewRule->addValue($mValue); } $this->addRule($oNewRule); } } /** * @return void * * @deprecated since 8.7.0, will be removed without substitution in version 9.0 in #511 */ public function createBackgroundShorthand() { $aProperties = [ 'background-color', 'background-image', 'background-repeat', 'background-position', 'background-attachment', ]; $this->createShorthandProperties($aProperties, 'background'); } /** * @return void * * @deprecated since 8.7.0, will be removed without substitution in version 9.0 in #511 */ public function createListStyleShorthand() { $aProperties = [ 'list-style-type', 'list-style-position', 'list-style-image', ]; $this->createShorthandProperties($aProperties, 'list-style'); } /** * Combines `border-color`, `border-style` and `border-width` into `border`. * * Should be run after `create_dimensions_shorthand`! * * @return void * * @deprecated since 8.7.0, will be removed without substitution in version 9.0 in #511 */ public function createBorderShorthand() { $aProperties = [ 'border-width', 'border-style', 'border-color', ]; $this->createShorthandProperties($aProperties, 'border'); } /** * Looks for long format CSS dimensional properties * (margin, padding, border-color, border-style and border-width) * and converts them into shorthand CSS properties. * * @return void * * @deprecated since 8.7.0, will be removed without substitution in version 9.0 in #511 */ public function createDimensionsShorthand() { $aPositions = ['top', 'right', 'bottom', 'left']; $aExpansions = [ 'margin' => 'margin-%s', 'padding' => 'padding-%s', 'border-color' => 'border-%s-color', 'border-style' => 'border-%s-style', 'border-width' => 'border-%s-width', ]; $aRules = $this->getRulesAssoc(); foreach ($aExpansions as $sProperty => $sExpanded) { $aFoldable = []; foreach ($aRules as $sRuleName => $oRule) { foreach ($aPositions as $sPosition) { if ($sRuleName == sprintf($sExpanded, $sPosition)) { $aFoldable[$sRuleName] = $oRule; } } } // All four dimensions must be present if (count($aFoldable) == 4) { $aValues = []; foreach ($aPositions as $sPosition) { $oRule = $aRules[sprintf($sExpanded, $sPosition)]; $mRuleValue = $oRule->getValue(); $aRuleValues = []; if (!$mRuleValue instanceof RuleValueList) { $aRuleValues[] = $mRuleValue; } else { $aRuleValues = $mRuleValue->getListComponents(); } $aValues[$sPosition] = $aRuleValues; } $oNewRule = new Rule($sProperty, $oRule->getLineNo(), $oRule->getColNo()); if ((string)$aValues['left'][0] == (string)$aValues['right'][0]) { if ((string)$aValues['top'][0] == (string)$aValues['bottom'][0]) { if ((string)$aValues['top'][0] == (string)$aValues['left'][0]) { // All 4 sides are equal $oNewRule->addValue($aValues['top']); } else { // Top and bottom are equal, left and right are equal $oNewRule->addValue($aValues['top']); $oNewRule->addValue($aValues['left']); } } else { // Only left and right are equal $oNewRule->addValue($aValues['top']); $oNewRule->addValue($aValues['left']); $oNewRule->addValue($aValues['bottom']); } } else { // No sides are equal $oNewRule->addValue($aValues['top']); $oNewRule->addValue($aValues['left']); $oNewRule->addValue($aValues['bottom']); $oNewRule->addValue($aValues['right']); } $this->addRule($oNewRule); foreach ($aPositions as $sPosition) { $this->removeRule(sprintf($sExpanded, $sPosition)); } } } } /** * Looks for long format CSS font properties (e.g. `font-weight`) and * tries to convert them into a shorthand CSS `font` property. * * At least `font-size` AND `font-family` must be present in order to create a shorthand declaration. * * @return void * * @deprecated since 8.7.0, will be removed without substitution in version 9.0 in #511 */ public function createFontShorthand() { $aFontProperties = [ 'font-style', 'font-variant', 'font-weight', 'font-size', 'line-height', 'font-family', ]; $aRules = $this->getRulesAssoc(); if (!isset($aRules['font-size']) || !isset($aRules['font-family'])) { return; } $oOldRule = isset($aRules['font-size']) ? $aRules['font-size'] : $aRules['font-family']; $oNewRule = new Rule('font', $oOldRule->getLineNo(), $oOldRule->getColNo()); unset($oOldRule); foreach (['font-style', 'font-variant', 'font-weight'] as $sProperty) { if (isset($aRules[$sProperty])) { $oRule = $aRules[$sProperty]; $mRuleValue = $oRule->getValue(); $aValues = []; if (!$mRuleValue instanceof RuleValueList) { $aValues[] = $mRuleValue; } else { $aValues = $mRuleValue->getListComponents(); } if ($aValues[0] !== 'normal') { $oNewRule->addValue($aValues[0]); } } } // Get the font-size value $oRule = $aRules['font-size']; $mRuleValue = $oRule->getValue(); $aFSValues = []; if (!$mRuleValue instanceof RuleValueList) { $aFSValues[] = $mRuleValue; } else { $aFSValues = $mRuleValue->getListComponents(); } // But wait to know if we have line-height to add it if (isset($aRules['line-height'])) { $oRule = $aRules['line-height']; $mRuleValue = $oRule->getValue(); $aLHValues = []; if (!$mRuleValue instanceof RuleValueList) { $aLHValues[] = $mRuleValue; } else { $aLHValues = $mRuleValue->getListComponents(); } if ($aLHValues[0] !== 'normal') { $val = new RuleValueList('/', $this->getLineNo()); $val->addListComponent($aFSValues[0]); $val->addListComponent($aLHValues[0]); $oNewRule->addValue($val); } } else { $oNewRule->addValue($aFSValues[0]); } $oRule = $aRules['font-family']; $mRuleValue = $oRule->getValue(); $aFFValues = []; if (!$mRuleValue instanceof RuleValueList) { $aFFValues[] = $mRuleValue; } else { $aFFValues = $mRuleValue->getListComponents(); } $oFFValue = new RuleValueList(',', $this->getLineNo()); $oFFValue->setListComponents($aFFValues); $oNewRule->addValue($oFFValue); $this->addRule($oNewRule); foreach ($aFontProperties as $sProperty) { $this->removeRule($sProperty); } } /** * @return string * * @throws OutputException * * @deprecated in V8.8.0, will be removed in V9.0.0. Use `render` instead. */ public function __toString() { return $this->render(new OutputFormat()); } /** * @param OutputFormat|null $oOutputFormat * * @return string * * @throws OutputException */ public function render($oOutputFormat) { $sResult = $oOutputFormat->comments($this); if (count($this->aSelectors) === 0) { // If all the selectors have been removed, this declaration block becomes invalid throw new OutputException( 'Attempt to print declaration block with missing selector', $this->getLineNumber() ); } $sResult .= $oOutputFormat->sBeforeDeclarationBlock; $sResult .= $oOutputFormat->implode( $oOutputFormat->spaceBeforeSelectorSeparator() . ',' . $oOutputFormat->spaceAfterSelectorSeparator(), $this->aSelectors ); $sResult .= $oOutputFormat->sAfterDeclarationBlockSelectors; $sResult .= $oOutputFormat->spaceBeforeOpeningBrace() . '{'; $sResult .= $this->renderRules($oOutputFormat); $sResult .= '}'; $sResult .= $oOutputFormat->sAfterDeclarationBlock; return $sResult; } } packages/Sabberworm/CSS/Value/Value.php 0000644 00000020200 15217646630 0013722 0 ustar 00 <?php namespace Automattic\WooCommerce\Vendor\Sabberworm\CSS\Value; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\CSSElement; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Parsing\ParserState; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Parsing\SourceException; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Parsing\UnexpectedEOFException; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Parsing\UnexpectedTokenException; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Position\Position; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Position\Positionable; /** * Abstract base class for specific classes of CSS values: `Size`, `Color`, `CSSString` and `URL`, and another * abstract subclass `ValueList`. */ abstract class Value implements CSSElement, Positionable { use Position; /** * @param int $iLineNo */ public function __construct($iLineNo = 0) { $this->setPosition($iLineNo); } /** * @param array<array-key, string> $aListDelimiters * * @return RuleValueList|CSSFunction|CSSString|LineName|Size|URL|string * * @throws UnexpectedTokenException * @throws UnexpectedEOFException * * @internal since V8.8.0 */ public static function parseValue(ParserState $oParserState, array $aListDelimiters = []) { /** @var array<int, RuleValueList|CSSFunction|CSSString|LineName|Size|URL|string> $aStack */ $aStack = []; $oParserState->consumeWhiteSpace(); //Build a list of delimiters and parsed values while ( !($oParserState->comes('}') || $oParserState->comes(';') || $oParserState->comes('!') || $oParserState->comes(')') || $oParserState->comes('\\') || $oParserState->isEnd()) ) { if (count($aStack) > 0) { $bFoundDelimiter = false; foreach ($aListDelimiters as $sDelimiter) { if ($oParserState->comes($sDelimiter)) { array_push($aStack, $oParserState->consume($sDelimiter)); $oParserState->consumeWhiteSpace(); $bFoundDelimiter = true; break; } } if (!$bFoundDelimiter) { //Whitespace was the list delimiter array_push($aStack, ' '); } } array_push($aStack, self::parsePrimitiveValue($oParserState)); $oParserState->consumeWhiteSpace(); } // Convert the list to list objects foreach ($aListDelimiters as $sDelimiter) { $iStackLength = count($aStack); if ($iStackLength === 1) { return $aStack[0]; } $aNewStack = []; for ($iStartPosition = 0; $iStartPosition < $iStackLength; ++$iStartPosition) { if ($iStartPosition === ($iStackLength - 1) || $sDelimiter !== $aStack[$iStartPosition + 1]) { $aNewStack[] = $aStack[$iStartPosition]; continue; } $iLength = 2; //Number of elements to be joined for ($i = $iStartPosition + 3; $i < $iStackLength; $i += 2, ++$iLength) { if ($sDelimiter !== $aStack[$i]) { break; } } $oList = new RuleValueList($sDelimiter, $oParserState->currentLine()); for ($i = $iStartPosition; $i - $iStartPosition < $iLength * 2; $i += 2) { $oList->addListComponent($aStack[$i]); } $aNewStack[] = $oList; $iStartPosition += $iLength * 2 - 2; } $aStack = $aNewStack; } if (!isset($aStack[0])) { throw new UnexpectedTokenException( " {$oParserState->peek()} ", $oParserState->peek(1, -1) . $oParserState->peek(2), 'literal', $oParserState->currentLine() ); } return $aStack[0]; } /** * @param bool $bIgnoreCase * * @return CSSFunction|string * * @throws UnexpectedEOFException * @throws UnexpectedTokenException * * @internal since V8.8.0 */ public static function parseIdentifierOrFunction(ParserState $oParserState, $bIgnoreCase = false) { $oAnchor = $oParserState->anchor(); $mResult = $oParserState->parseIdentifier($bIgnoreCase); if ($oParserState->comes('(')) { $oAnchor->backtrack(); if ($oParserState->streql('url', $mResult)) { $mResult = URL::parse($oParserState); } elseif ( $oParserState->streql('calc', $mResult) || $oParserState->streql('-webkit-calc', $mResult) || $oParserState->streql('-moz-calc', $mResult) ) { $mResult = CalcFunction::parse($oParserState); } else { $mResult = CSSFunction::parse($oParserState, $bIgnoreCase); } } return $mResult; } /** * @return CSSFunction|CSSString|LineName|Size|URL|string * * @throws UnexpectedEOFException * @throws UnexpectedTokenException * @throws SourceException * * @internal since V8.8.0 */ public static function parsePrimitiveValue(ParserState $oParserState) { $oValue = null; $oParserState->consumeWhiteSpace(); if ( is_numeric($oParserState->peek()) || ($oParserState->comes('-.') && is_numeric($oParserState->peek(1, 2))) || (($oParserState->comes('-') || $oParserState->comes('.')) && is_numeric($oParserState->peek(1, 1))) ) { $oValue = Size::parse($oParserState); } elseif ($oParserState->comes('#') || $oParserState->comes('rgb', true) || $oParserState->comes('hsl', true)) { $oValue = Color::parse($oParserState); } elseif ($oParserState->comes("'") || $oParserState->comes('"')) { $oValue = CSSString::parse($oParserState); } elseif ($oParserState->comes("progid:") && $oParserState->getSettings()->bLenientParsing) { $oValue = self::parseMicrosoftFilter($oParserState); } elseif ($oParserState->comes("[")) { $oValue = LineName::parse($oParserState); } elseif ($oParserState->comes("U+")) { $oValue = self::parseUnicodeRangeValue($oParserState); } else { $sNextChar = $oParserState->peek(1); try { $oValue = self::parseIdentifierOrFunction($oParserState); } catch (UnexpectedTokenException $e) { if (\in_array($sNextChar, ['+', '-', '*', '/'], true)) { $oValue = $oParserState->consume(1); } else { throw $e; } } } $oParserState->consumeWhiteSpace(); return $oValue; } /** * @return CSSFunction * * @throws UnexpectedEOFException * @throws UnexpectedTokenException */ private static function parseMicrosoftFilter(ParserState $oParserState) { $sFunction = $oParserState->consumeUntil('(', false, true); $aArguments = Value::parseValue($oParserState, [',', '=']); return new CSSFunction($sFunction, $aArguments, ',', $oParserState->currentLine()); } /** * @return string * * @throws UnexpectedEOFException * @throws UnexpectedTokenException */ private static function parseUnicodeRangeValue(ParserState $oParserState) { $iCodepointMaxLength = 6; // Code points outside BMP can use up to six digits $sRange = ""; $oParserState->consume("U+"); do { if ($oParserState->comes('-')) { $iCodepointMaxLength = 13; // Max length is 2 six digit code points + the dash(-) between them } $sRange .= $oParserState->consume(1); } while (strlen($sRange) < $iCodepointMaxLength && preg_match("/[A-Fa-f0-9\?-]/", $oParserState->peek())); return "U+{$sRange}"; } } packages/Sabberworm/CSS/Value/RuleValueList.php 0000644 00000001112 15217646630 0015407 0 ustar 00 <?php namespace Automattic\WooCommerce\Vendor\Sabberworm\CSS\Value; /** * This class is used to represent all multivalued rules like `font: bold 12px/3 Helvetica, Verdana, sans-serif;` * (where the value would be a whitespace-separated list of the primitive value `bold`, a slash-separated list * and a comma-separated list). */ class RuleValueList extends ValueList { /** * @param string $sSeparator * @param int $iLineNo */ public function __construct($sSeparator = ',', $iLineNo = 0) { parent::__construct([], $sSeparator, $iLineNo); } } packages/Sabberworm/CSS/Value/Color.php 0000644 00000015041 15217646630 0013733 0 ustar 00 <?php namespace Automattic\WooCommerce\Vendor\Sabberworm\CSS\Value; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\OutputFormat; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Parsing\ParserState; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Parsing\UnexpectedEOFException; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Parsing\UnexpectedTokenException; /** * `Color's can be input in the form #rrggbb, #rgb or schema(val1, val2, …) but are always stored as an array of * ('s' => val1, 'c' => val2, 'h' => val3, …) and output in the second form. */ class Color extends CSSFunction { /** * @param array<int, RuleValueList|CSSFunction|CSSString|LineName|Size|URL|string> $aColor * @param int $iLineNo */ public function __construct(array $aColor, $iLineNo = 0) { parent::__construct(implode('', array_keys($aColor)), $aColor, ',', $iLineNo); } /** * @param ParserState $oParserState * @param bool $bIgnoreCase * * @return Color|CSSFunction * * @throws UnexpectedEOFException * @throws UnexpectedTokenException * * @internal since V8.8.0 */ public static function parse(ParserState $oParserState, $bIgnoreCase = false) { $aColor = []; if ($oParserState->comes('#')) { $oParserState->consume('#'); $sValue = $oParserState->parseIdentifier(false); if ($oParserState->strlen($sValue) === 3) { $sValue = $sValue[0] . $sValue[0] . $sValue[1] . $sValue[1] . $sValue[2] . $sValue[2]; } elseif ($oParserState->strlen($sValue) === 4) { $sValue = $sValue[0] . $sValue[0] . $sValue[1] . $sValue[1] . $sValue[2] . $sValue[2] . $sValue[3] . $sValue[3]; } if ($oParserState->strlen($sValue) === 8) { $aColor = [ 'r' => new Size(intval($sValue[0] . $sValue[1], 16), null, true, $oParserState->currentLine()), 'g' => new Size(intval($sValue[2] . $sValue[3], 16), null, true, $oParserState->currentLine()), 'b' => new Size(intval($sValue[4] . $sValue[5], 16), null, true, $oParserState->currentLine()), 'a' => new Size( round(self::mapRange(intval($sValue[6] . $sValue[7], 16), 0, 255, 0, 1), 2), null, true, $oParserState->currentLine() ), ]; } elseif ($oParserState->strlen($sValue) === 6) { $aColor = [ 'r' => new Size(intval($sValue[0] . $sValue[1], 16), null, true, $oParserState->currentLine()), 'g' => new Size(intval($sValue[2] . $sValue[3], 16), null, true, $oParserState->currentLine()), 'b' => new Size(intval($sValue[4] . $sValue[5], 16), null, true, $oParserState->currentLine()), ]; } else { throw new UnexpectedTokenException( 'Invalid hex color value', $sValue, 'custom', $oParserState->currentLine() ); } } else { $sColorMode = $oParserState->parseIdentifier(true); $oParserState->consumeWhiteSpace(); $oParserState->consume('('); $bContainsVar = false; $iLength = $oParserState->strlen($sColorMode); for ($i = 0; $i < $iLength; ++$i) { $oParserState->consumeWhiteSpace(); if ($oParserState->comes('var')) { $aColor[$sColorMode[$i]] = CSSFunction::parseIdentifierOrFunction($oParserState); $bContainsVar = true; } else { $aColor[$sColorMode[$i]] = Size::parse($oParserState, true); } if ($bContainsVar && $oParserState->comes(')')) { // With a var argument the function can have fewer arguments break; } $oParserState->consumeWhiteSpace(); if ($i < ($iLength - 1)) { $oParserState->consume(','); } } $oParserState->consume(')'); if ($bContainsVar) { return new CSSFunction($sColorMode, array_values($aColor), ',', $oParserState->currentLine()); } } return new Color($aColor, $oParserState->currentLine()); } /** * @param float $fVal * @param float $fFromMin * @param float $fFromMax * @param float $fToMin * @param float $fToMax * * @return float */ private static function mapRange($fVal, $fFromMin, $fFromMax, $fToMin, $fToMax) { $fFromRange = $fFromMax - $fFromMin; $fToRange = $fToMax - $fToMin; $fMultiplier = $fToRange / $fFromRange; $fNewVal = $fVal - $fFromMin; $fNewVal *= $fMultiplier; return $fNewVal + $fToMin; } /** * @return array<int, RuleValueList|CSSFunction|CSSString|LineName|Size|URL|string> */ public function getColor() { return $this->aComponents; } /** * @param array<int, RuleValueList|CSSFunction|CSSString|LineName|Size|URL|string> $aColor * * @return void */ public function setColor(array $aColor) { $this->setName(implode('', array_keys($aColor))); $this->aComponents = $aColor; } /** * @return string */ public function getColorDescription() { return $this->getName(); } /** * @return string * * @deprecated in V8.8.0, will be removed in V9.0.0. Use `render` instead. */ public function __toString() { return $this->render(new OutputFormat()); } /** * @param OutputFormat|null $oOutputFormat * * @return string */ public function render($oOutputFormat) { // Shorthand RGB color values if ($oOutputFormat->getRGBHashNotation() && implode('', array_keys($this->aComponents)) === 'rgb') { $sResult = sprintf( '%02x%02x%02x', $this->aComponents['r']->getSize(), $this->aComponents['g']->getSize(), $this->aComponents['b']->getSize() ); return '#' . (($sResult[0] == $sResult[1]) && ($sResult[2] == $sResult[3]) && ($sResult[4] == $sResult[5]) ? "$sResult[0]$sResult[2]$sResult[4]" : $sResult); } return parent::render($oOutputFormat); } } packages/Sabberworm/CSS/Value/CalcFunction.php 0000644 00000010647 15217646630 0015234 0 ustar 00 <?php namespace Automattic\WooCommerce\Vendor\Sabberworm\CSS\Value; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Parsing\ParserState; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Parsing\UnexpectedEOFException; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Parsing\UnexpectedTokenException; /** * Support for `-webkit-calc` and `-moz-calc` is deprecated in version 8.8.0, and will be removed in version 9.0.0. */ class CalcFunction extends CSSFunction { /** * @var int * * @internal */ const T_OPERAND = 1; /** * @var int * * @internal */ const T_OPERATOR = 2; /** * @param ParserState $oParserState * @param bool $bIgnoreCase * * @return CalcFunction * * @throws UnexpectedTokenException * @throws UnexpectedEOFException * * @internal since V8.8.0 */ public static function parse(ParserState $oParserState, $bIgnoreCase = false) { $aOperators = ['+', '-', '*', '/']; $sFunction = $oParserState->parseIdentifier(); if ($oParserState->peek() != '(') { // Found ; or end of line before an opening bracket throw new UnexpectedTokenException('(', $oParserState->peek(), 'literal', $oParserState->currentLine()); } elseif (!in_array($sFunction, ['calc', '-moz-calc', '-webkit-calc'])) { // Found invalid calc definition. Example calc (... throw new UnexpectedTokenException('calc', $sFunction, 'literal', $oParserState->currentLine()); } $oParserState->consume('('); $oCalcList = new CalcRuleValueList($oParserState->currentLine()); $oList = new RuleValueList(',', $oParserState->currentLine()); $iNestingLevel = 0; $iLastComponentType = null; while (!$oParserState->comes(')') || $iNestingLevel > 0) { if ($oParserState->isEnd() && $iNestingLevel === 0) { break; } $oParserState->consumeWhiteSpace(); if ($oParserState->comes('(')) { $iNestingLevel++; $oCalcList->addListComponent($oParserState->consume(1)); $oParserState->consumeWhiteSpace(); continue; } elseif ($oParserState->comes(')')) { $iNestingLevel--; $oCalcList->addListComponent($oParserState->consume(1)); $oParserState->consumeWhiteSpace(); continue; } if ($iLastComponentType != CalcFunction::T_OPERAND) { $oVal = Value::parsePrimitiveValue($oParserState); $oCalcList->addListComponent($oVal); $iLastComponentType = CalcFunction::T_OPERAND; } else { if (in_array($oParserState->peek(), $aOperators)) { if (($oParserState->comes('-') || $oParserState->comes('+'))) { if ( $oParserState->peek(1, -1) != ' ' || !($oParserState->comes('- ') || $oParserState->comes('+ ')) ) { throw new UnexpectedTokenException( " {$oParserState->peek()} ", $oParserState->peek(1, -1) . $oParserState->peek(2), 'literal', $oParserState->currentLine() ); } } $oCalcList->addListComponent($oParserState->consume(1)); $iLastComponentType = CalcFunction::T_OPERATOR; } else { throw new UnexpectedTokenException( sprintf( 'Next token was expected to be an operand of type %s. Instead "%s" was found.', implode(', ', $aOperators), $oParserState->peek() ), '', 'custom', $oParserState->currentLine() ); } } $oParserState->consumeWhiteSpace(); } $oList->addListComponent($oCalcList); if (!$oParserState->isEnd()) { $oParserState->consume(')'); } return new CalcFunction($sFunction, $oList, ',', $oParserState->currentLine()); } } packages/Sabberworm/CSS/Value/URL.php 0000644 00000004755 15217646630 0013331 0 ustar 00 <?php namespace Automattic\WooCommerce\Vendor\Sabberworm\CSS\Value; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\OutputFormat; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Parsing\ParserState; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Parsing\SourceException; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Parsing\UnexpectedEOFException; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Parsing\UnexpectedTokenException; /** * This class represents URLs in CSS. `URL`s always output in `URL("")` notation. */ class URL extends PrimitiveValue { /** * @var CSSString */ private $oURL; /** * @param int $iLineNo */ public function __construct(CSSString $oURL, $iLineNo = 0) { parent::__construct($iLineNo); $this->oURL = $oURL; } /** * @return URL * * @throws SourceException * @throws UnexpectedEOFException * @throws UnexpectedTokenException * * @internal since V8.8.0 */ public static function parse(ParserState $oParserState) { $oAnchor = $oParserState->anchor(); $sIdentifier = ''; for ($i = 0; $i < 3; $i++) { $sChar = $oParserState->parseCharacter(true); if ($sChar === null) { break; } $sIdentifier .= $sChar; } $bUseUrl = $oParserState->streql($sIdentifier, 'url'); if ($bUseUrl) { $oParserState->consumeWhiteSpace(); $oParserState->consume('('); } else { $oAnchor->backtrack(); } $oParserState->consumeWhiteSpace(); $oResult = new URL(CSSString::parse($oParserState), $oParserState->currentLine()); if ($bUseUrl) { $oParserState->consumeWhiteSpace(); $oParserState->consume(')'); } return $oResult; } /** * @return void */ public function setURL(CSSString $oURL) { $this->oURL = $oURL; } /** * @return CSSString */ public function getURL() { return $this->oURL; } /** * @return string * * @deprecated in V8.8.0, will be removed in V9.0.0. Use `render` instead. */ public function __toString() { return $this->render(new OutputFormat()); } /** * @param OutputFormat|null $oOutputFormat * * @return string */ public function render($oOutputFormat) { return "url({$this->oURL->render($oOutputFormat)})"; } } packages/Sabberworm/CSS/Value/PrimitiveValue.php 0000644 00000000402 15217646630 0015615 0 ustar 00 <?php namespace Automattic\WooCommerce\Vendor\Sabberworm\CSS\Value; abstract class PrimitiveValue extends Value { /** * @param int $iLineNo */ public function __construct($iLineNo = 0) { parent::__construct($iLineNo); } } packages/Sabberworm/CSS/Value/LineName.php 0000644 00000004103 15217646630 0014342 0 ustar 00 <?php namespace Automattic\WooCommerce\Vendor\Sabberworm\CSS\Value; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\OutputFormat; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Parsing\ParserState; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Parsing\UnexpectedEOFException; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Parsing\UnexpectedTokenException; class LineName extends ValueList { /** * @param array<int, RuleValueList|CSSFunction|CSSString|LineName|Size|URL|string> $aComponents * @param int $iLineNo */ public function __construct(array $aComponents = [], $iLineNo = 0) { parent::__construct($aComponents, ' ', $iLineNo); } /** * @return LineName * * @throws UnexpectedTokenException * @throws UnexpectedEOFException * * @internal since V8.8.0 */ public static function parse(ParserState $oParserState) { $oParserState->consume('['); $oParserState->consumeWhiteSpace(); $aNames = []; do { if ($oParserState->getSettings()->bLenientParsing) { try { $aNames[] = $oParserState->parseIdentifier(); } catch (UnexpectedTokenException $e) { if (!$oParserState->comes(']')) { throw $e; } } } else { $aNames[] = $oParserState->parseIdentifier(); } $oParserState->consumeWhiteSpace(); } while (!$oParserState->comes(']')); $oParserState->consume(']'); return new LineName($aNames, $oParserState->currentLine()); } /** * @return string * * @deprecated in V8.8.0, will be removed in V9.0.0. Use `render` instead. */ public function __toString() { return $this->render(new OutputFormat()); } /** * @param OutputFormat|null $oOutputFormat * * @return string */ public function render($oOutputFormat) { return '[' . parent::render(OutputFormat::createCompact()) . ']'; } } packages/Sabberworm/CSS/Value/CSSString.php 0000644 00000006215 15217646630 0014477 0 ustar 00 <?php namespace Automattic\WooCommerce\Vendor\Sabberworm\CSS\Value; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\OutputFormat; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Parsing\ParserState; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Parsing\SourceException; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Parsing\UnexpectedEOFException; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Parsing\UnexpectedTokenException; /** * This class is a wrapper for quoted strings to distinguish them from keywords. * * `CSSString`s always output with double quotes. */ class CSSString extends PrimitiveValue { /** * @var string */ private $sString; /** * @param string $sString * @param int $iLineNo */ public function __construct($sString, $iLineNo = 0) { $this->sString = $sString; parent::__construct($iLineNo); } /** * @return CSSString * * @throws SourceException * @throws UnexpectedEOFException * @throws UnexpectedTokenException * * @internal since V8.8.0 */ public static function parse(ParserState $oParserState) { $sBegin = $oParserState->peek(); $sQuote = null; if ($sBegin === "'") { $sQuote = "'"; } elseif ($sBegin === '"') { $sQuote = '"'; } if ($sQuote !== null) { $oParserState->consume($sQuote); } $sResult = ""; $sContent = null; if ($sQuote === null) { // Unquoted strings end in whitespace or with braces, brackets, parentheses while (!preg_match('/[\\s{}()<>\\[\\]]/isu', $oParserState->peek())) { $sResult .= $oParserState->parseCharacter(false); } } else { while (!$oParserState->comes($sQuote)) { $sContent = $oParserState->parseCharacter(false); if ($sContent === null) { throw new SourceException( "Non-well-formed quoted string {$oParserState->peek(3)}", $oParserState->currentLine() ); } $sResult .= $sContent; } $oParserState->consume($sQuote); } return new CSSString($sResult, $oParserState->currentLine()); } /** * @param string $sString * * @return void */ public function setString($sString) { $this->sString = $sString; } /** * @return string */ public function getString() { return $this->sString; } /** * @return string * * @deprecated in V8.8.0, will be removed in V9.0.0. Use `render` instead. */ public function __toString() { return $this->render(new OutputFormat()); } /** * @param OutputFormat|null $oOutputFormat * * @return string */ public function render($oOutputFormat) { $sString = addslashes($this->sString); $sString = str_replace("\n", '\A', $sString); return $oOutputFormat->getStringQuotingType() . $sString . $oOutputFormat->getStringQuotingType(); } } packages/Sabberworm/CSS/Value/CSSFunction.php 0000644 00000006016 15217646631 0015016 0 ustar 00 <?php namespace Automattic\WooCommerce\Vendor\Sabberworm\CSS\Value; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\OutputFormat; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Parsing\ParserState; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Parsing\SourceException; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Parsing\UnexpectedEOFException; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Parsing\UnexpectedTokenException; /** * A `CSSFunction` represents a special kind of value that also contains a function name and where the values are the * function’s arguments. It also handles equals-sign-separated argument lists like `filter: alpha(opacity=90);`. */ class CSSFunction extends ValueList { /** * @var string * * @internal since 8.8.0 */ protected $sName; /** * @param string $sName * @param RuleValueList|array<int, RuleValueList|CSSFunction|CSSString|LineName|Size|URL|string> $aArguments * @param string $sSeparator * @param int $iLineNo */ public function __construct($sName, $aArguments, $sSeparator = ',', $iLineNo = 0) { if ($aArguments instanceof RuleValueList) { $sSeparator = $aArguments->getListSeparator(); $aArguments = $aArguments->getListComponents(); } $this->sName = $sName; $this->setPosition($iLineNo); // TODO: redundant? parent::__construct($aArguments, $sSeparator, $iLineNo); } /** * @param ParserState $oParserState * @param bool $bIgnoreCase * * @return CSSFunction * * @throws SourceException * @throws UnexpectedEOFException * @throws UnexpectedTokenException * * @internal since V8.8.0 */ public static function parse(ParserState $oParserState, $bIgnoreCase = false) { $mResult = $oParserState->parseIdentifier($bIgnoreCase); $oParserState->consume('('); $aArguments = Value::parseValue($oParserState, ['=', ' ', ',']); $mResult = new CSSFunction($mResult, $aArguments, ',', $oParserState->currentLine()); $oParserState->consume(')'); return $mResult; } /** * @return string */ public function getName() { return $this->sName; } /** * @param string $sName * * @return void */ public function setName($sName) { $this->sName = $sName; } /** * @return array<int, RuleValueList|CSSFunction|CSSString|LineName|Size|URL|string> */ public function getArguments() { return $this->aComponents; } /** * @return string * * @deprecated in V8.8.0, will be removed in V9.0.0. Use `render` instead. */ public function __toString() { return $this->render(new OutputFormat()); } /** * @param OutputFormat|null $oOutputFormat * * @return string */ public function render($oOutputFormat) { $aArguments = parent::render($oOutputFormat); return "{$this->sName}({$aArguments})"; } } packages/Sabberworm/CSS/Value/Size.php 0000644 00000014412 15217646631 0013571 0 ustar 00 <?php namespace Automattic\WooCommerce\Vendor\Sabberworm\CSS\Value; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\OutputFormat; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Parsing\ParserState; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Parsing\UnexpectedEOFException; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Parsing\UnexpectedTokenException; /** * A `Size` consists of a numeric `size` value and a unit. */ class Size extends PrimitiveValue { /** * vh/vw/vm(ax)/vmin/rem are absolute insofar as they don’t scale to the immediate parent (only the viewport) * * @var array<int, string> * * @internal */ const ABSOLUTE_SIZE_UNITS = [ 'px', 'pt', 'pc', 'cm', 'mm', 'mozmm', 'in', 'vh', 'dvh', 'svh', 'lvh', 'vw', 'vmin', 'vmax', 'rem', ]; /** * @var array<int, string> * * @internal */ const RELATIVE_SIZE_UNITS = ['%', 'em', 'ex', 'ch', 'fr']; /** * @var array<int, string> * * @internal */ const NON_SIZE_UNITS = ['deg', 'grad', 'rad', 's', 'ms', 'turn', 'Hz', 'kHz']; /** * @var array<int, array<string, string>>|null */ private static $SIZE_UNITS = null; /** * @var float */ private $fSize; /** * @var string|null */ private $sUnit; /** * @var bool */ private $bIsColorComponent; /** * @param float|int|string $fSize * @param string|null $sUnit * @param bool $bIsColorComponent * @param int $iLineNo */ public function __construct($fSize, $sUnit = null, $bIsColorComponent = false, $iLineNo = 0) { parent::__construct($iLineNo); $this->fSize = (float)$fSize; $this->sUnit = $sUnit; $this->bIsColorComponent = $bIsColorComponent; } /** * @param bool $bIsColorComponent * * @return Size * * @throws UnexpectedEOFException * @throws UnexpectedTokenException * * @internal since V8.8.0 */ public static function parse(ParserState $oParserState, $bIsColorComponent = false) { $sSize = ''; if ($oParserState->comes('-')) { $sSize .= $oParserState->consume('-'); } while (is_numeric($oParserState->peek()) || $oParserState->comes('.') || $oParserState->comes('e', true)) { if ($oParserState->comes('.')) { $sSize .= $oParserState->consume('.'); } elseif ($oParserState->comes('e', true)) { $sLookahead = $oParserState->peek(1, 1); if (is_numeric($sLookahead) || $sLookahead === '+' || $sLookahead === '-') { $sSize .= $oParserState->consume(2); } else { break; // Reached the unit part of the number like "em" or "ex" } } else { $sSize .= $oParserState->consume(1); } } $sUnit = null; $aSizeUnits = self::getSizeUnits(); foreach ($aSizeUnits as $iLength => &$aValues) { $sKey = strtolower($oParserState->peek($iLength)); if (array_key_exists($sKey, $aValues)) { if (($sUnit = $aValues[$sKey]) !== null) { $oParserState->consume($iLength); break; } } } return new Size((float)$sSize, $sUnit, $bIsColorComponent, $oParserState->currentLine()); } /** * @return array<int, array<string, string>> */ private static function getSizeUnits() { if (!is_array(self::$SIZE_UNITS)) { self::$SIZE_UNITS = []; foreach (array_merge(self::ABSOLUTE_SIZE_UNITS, self::RELATIVE_SIZE_UNITS, self::NON_SIZE_UNITS) as $val) { $iSize = strlen($val); if (!isset(self::$SIZE_UNITS[$iSize])) { self::$SIZE_UNITS[$iSize] = []; } self::$SIZE_UNITS[$iSize][strtolower($val)] = $val; } krsort(self::$SIZE_UNITS, SORT_NUMERIC); } return self::$SIZE_UNITS; } /** * @param string $sUnit * * @return void */ public function setUnit($sUnit) { $this->sUnit = $sUnit; } /** * @return string|null */ public function getUnit() { return $this->sUnit; } /** * @param float|int|string $fSize */ public function setSize($fSize) { $this->fSize = (float)$fSize; } /** * @return float */ public function getSize() { return $this->fSize; } /** * @return bool */ public function isColorComponent() { return $this->bIsColorComponent; } /** * Returns whether the number stored in this Size really represents a size (as in a length of something on screen). * * @return false if the unit an angle, a duration, a frequency or the number is a component in a Color object. */ public function isSize() { if (in_array($this->sUnit, self::NON_SIZE_UNITS, true)) { return false; } return !$this->isColorComponent(); } /** * @return bool */ public function isRelative() { if (in_array($this->sUnit, self::RELATIVE_SIZE_UNITS, true)) { return true; } if ($this->sUnit === null && $this->fSize != 0) { return true; } return false; } /** * @return string * * @deprecated in V8.8.0, will be removed in V9.0.0. Use `render` instead. */ public function __toString() { return $this->render(new OutputFormat()); } /** * @param OutputFormat|null $oOutputFormat * * @return string */ public function render($oOutputFormat) { $l = localeconv(); $sPoint = preg_quote($l['decimal_point'], '/'); $sSize = preg_match("/[\d\.]+e[+-]?\d+/i", (string)$this->fSize) ? preg_replace("/$sPoint?0+$/", "", sprintf("%f", $this->fSize)) : (string)$this->fSize; return preg_replace(["/$sPoint/", "/^(-?)0\./"], ['.', '$1.'], $sSize) . ($this->sUnit === null ? '' : $this->sUnit); } } packages/Sabberworm/CSS/Value/CalcRuleValueList.php 0000644 00000001036 15217646631 0016200 0 ustar 00 <?php namespace Automattic\WooCommerce\Vendor\Sabberworm\CSS\Value; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\OutputFormat; class CalcRuleValueList extends RuleValueList { /** * @param int $iLineNo */ public function __construct($iLineNo = 0) { parent::__construct(',', $iLineNo); } /** * @param OutputFormat|null $oOutputFormat * * @return string */ public function render($oOutputFormat) { return $oOutputFormat->implode(' ', $this->aComponents); } } packages/Sabberworm/CSS/Value/ValueList.php 0000644 00000005463 15217646631 0014575 0 ustar 00 <?php namespace Automattic\WooCommerce\Vendor\Sabberworm\CSS\Value; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\OutputFormat; /** * A `ValueList` represents a lists of `Value`s, separated by some separation character * (mostly `,`, whitespace, or `/`). * * There are two types of `ValueList`s: `RuleValueList` and `CSSFunction` */ abstract class ValueList extends Value { /** * @var array<int, RuleValueList|CSSFunction|CSSString|LineName|Size|URL|string> * * @internal since 8.8.0 */ protected $aComponents; /** * @var string * * @internal since 8.8.0 */ protected $sSeparator; /** * phpcs:ignore Generic.Files.LineLength * @param array<int, RuleValueList|CSSFunction|CSSString|LineName|Size|URL|string>|RuleValueList|CSSFunction|CSSString|LineName|Size|URL|string $aComponents * @param string $sSeparator * @param int $iLineNo */ public function __construct($aComponents = [], $sSeparator = ',', $iLineNo = 0) { parent::__construct($iLineNo); if (!is_array($aComponents)) { $aComponents = [$aComponents]; } $this->aComponents = $aComponents; $this->sSeparator = $sSeparator; } /** * @param RuleValueList|CSSFunction|CSSString|LineName|Size|URL|string $mComponent * * @return void */ public function addListComponent($mComponent) { $this->aComponents[] = $mComponent; } /** * @return array<int, RuleValueList|CSSFunction|CSSString|LineName|Size|URL|string> */ public function getListComponents() { return $this->aComponents; } /** * @param array<int, RuleValueList|CSSFunction|CSSString|LineName|Size|URL|string> $aComponents * * @return void */ public function setListComponents(array $aComponents) { $this->aComponents = $aComponents; } /** * @return string */ public function getListSeparator() { return $this->sSeparator; } /** * @param string $sSeparator * * @return void */ public function setListSeparator($sSeparator) { $this->sSeparator = $sSeparator; } /** * @return string * * @deprecated in V8.8.0, will be removed in V9.0.0. Use `render` instead. */ public function __toString() { return $this->render(new OutputFormat()); } /** * @param OutputFormat|null $oOutputFormat * * @return string */ public function render($oOutputFormat) { return $oOutputFormat->implode( $oOutputFormat->spaceBeforeListArgumentSeparator($this->sSeparator) . $this->sSeparator . $oOutputFormat->spaceAfterListArgumentSeparator($this->sSeparator), $this->aComponents ); } } packages/Sabberworm/CSS/Settings.php 0000644 00000005157 15217646631 0013411 0 ustar 00 <?php namespace Automattic\WooCommerce\Vendor\Sabberworm\CSS; /** * Parser settings class. * * Configure parser behaviour here. */ class Settings { /** * Multi-byte string support. * * If `true` (`mbstring` extension must be enabled), will use (slower) `mb_strlen`, `mb_convert_case`, `mb_substr` * and `mb_strpos` functions. Otherwise, the normal (ASCII-Only) functions will be used. * * @var bool * * @internal since 8.8.0, will be made private in 9.0.0 */ public $bMultibyteSupport; /** * The default charset for the CSS if no `@charset` declaration is found. Defaults to utf-8. * * @var string * * @internal since 8.8.0, will be made private in 9.0.0 */ public $sDefaultCharset = 'utf-8'; /** * Whether the parser silently ignore invalid rules instead of choking on them. * * @var bool * * @internal since 8.8.0, will be made private in 9.0.0 */ public $bLenientParsing = true; private function __construct() { $this->bMultibyteSupport = extension_loaded('mbstring'); } /** * @return self new instance */ public static function create() { return new Settings(); } /** * Enables/disables multi-byte string support. * * If `true` (`mbstring` extension must be enabled), will use (slower) `mb_strlen`, `mb_convert_case`, `mb_substr` * and `mb_strpos` functions. Otherwise, the normal (ASCII-Only) functions will be used. * * @param bool $bMultibyteSupport * * @return self fluent interface */ public function withMultibyteSupport($bMultibyteSupport = true) { $this->bMultibyteSupport = $bMultibyteSupport; return $this; } /** * Sets the charset to be used if the CSS does not contain an `@charset` declaration. * * @param string $sDefaultCharset * * @return self fluent interface */ public function withDefaultCharset($sDefaultCharset) { $this->sDefaultCharset = $sDefaultCharset; return $this; } /** * Configures whether the parser should silently ignore invalid rules. * * @param bool $bLenientParsing * * @return self fluent interface */ public function withLenientParsing($bLenientParsing = true) { $this->bLenientParsing = $bLenientParsing; return $this; } /** * Configures the parser to choke on invalid rules. * * @return self fluent interface */ public function beStrict() { return $this->withLenientParsing(false); } } packages/Sabberworm/CSS/Parsing/ParserState.php 0000644 00000034341 15217646631 0015446 0 ustar 00 <?php namespace Automattic\WooCommerce\Vendor\Sabberworm\CSS\Parsing; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Comment\Comment; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Settings; /** * @internal since 8.7.0 */ class ParserState { /** * @var null * * @internal since 8.5.2 */ const EOF = null; /** * @var Settings */ private $oParserSettings; /** * @var string */ private $sText; /** * @var array<int, string> */ private $aText; /** * @var int */ private $iCurrentPosition; /** * will only be used if the CSS does not contain an `@charset` declaration * * @var string */ private $sCharset; /** * @var int */ private $iLength; /** * @var int */ private $iLineNo; /** * @param string $sText the complete CSS as text (i.e., usually the contents of a CSS file) * @param int $iLineNo */ public function __construct($sText, Settings $oParserSettings, $iLineNo = 1) { $this->oParserSettings = $oParserSettings; $this->sText = $sText; $this->iCurrentPosition = 0; $this->iLineNo = $iLineNo; $this->setCharset($this->oParserSettings->sDefaultCharset); } /** * Sets the charset to be used if the CSS does not contain an `@charset` declaration. * * @param string $sCharset * * @return void */ public function setCharset($sCharset) { $this->sCharset = $sCharset; $this->aText = $this->strsplit($this->sText); if (is_array($this->aText)) { $this->iLength = count($this->aText); } } /** * Returns the charset that is used if the CSS does not contain an `@charset` declaration. * * @return string */ public function getCharset() { return $this->sCharset; } /** * @return int */ public function currentLine() { return $this->iLineNo; } /** * @return int */ public function currentColumn() { return $this->iCurrentPosition; } /** * @return Settings */ public function getSettings() { return $this->oParserSettings; } /** * @return \Automattic\WooCommerce\Vendor\Sabberworm\CSS\Parsing\Anchor */ public function anchor() { return new Anchor($this->iCurrentPosition, $this); } /** * @param int $iPosition * * @return void */ public function setPosition($iPosition) { $this->iCurrentPosition = $iPosition; } /** * @param bool $bIgnoreCase * * @return string * * @throws UnexpectedTokenException * * @internal since V8.8.0 */ public function parseIdentifier($bIgnoreCase = true) { if ($this->isEnd()) { throw new UnexpectedEOFException('', '', 'identifier', $this->iLineNo); } $sResult = $this->parseCharacter(true); if ($sResult === null) { throw new UnexpectedTokenException($sResult, $this->peek(5), 'identifier', $this->iLineNo); } $sCharacter = null; while (!$this->isEnd() && ($sCharacter = $this->parseCharacter(true)) !== null) { if (preg_match('/[a-zA-Z0-9\x{00A0}-\x{FFFF}_-]/Sux', $sCharacter)) { $sResult .= $sCharacter; } else { $sResult .= '\\' . $sCharacter; } } if ($bIgnoreCase) { $sResult = $this->strtolower($sResult); } return $sResult; } /** * @param bool $bIsForIdentifier * * @return string|null * * @throws UnexpectedEOFException * @throws UnexpectedTokenException * * @internal since V8.8.0 */ public function parseCharacter($bIsForIdentifier) { if ($this->peek() === '\\') { if ( $bIsForIdentifier && $this->oParserSettings->bLenientParsing && ($this->comes('\0') || $this->comes('\9')) ) { // Non-strings can contain \0 or \9 which is an IE hack supported in lenient parsing. return null; } $this->consume('\\'); if ($this->comes('\n') || $this->comes('\r')) { return ''; } if (preg_match('/[0-9a-fA-F]/Su', $this->peek()) === 0) { return $this->consume(1); } $sUnicode = $this->consumeExpression('/^[0-9a-fA-F]{1,6}/u', 6); if ($this->strlen($sUnicode) < 6) { // Consume whitespace after incomplete unicode escape if (preg_match('/\\s/isSu', $this->peek())) { if ($this->comes('\r\n')) { $this->consume(2); } else { $this->consume(1); } } } $iUnicode = intval($sUnicode, 16); $sUtf32 = ""; for ($i = 0; $i < 4; ++$i) { $sUtf32 .= chr($iUnicode & 0xff); $iUnicode = $iUnicode >> 8; } return iconv('utf-32le', $this->sCharset, $sUtf32); } if ($bIsForIdentifier) { $peek = ord($this->peek()); // Ranges: a-z A-Z 0-9 - _ if ( ($peek >= 97 && $peek <= 122) || ($peek >= 65 && $peek <= 90) || ($peek >= 48 && $peek <= 57) || ($peek === 45) || ($peek === 95) || ($peek > 0xa1) ) { return $this->consume(1); } } else { return $this->consume(1); } return null; } /** * @return array<int, Comment>|void * * @throws UnexpectedEOFException * @throws UnexpectedTokenException */ public function consumeWhiteSpace() { $aComments = []; do { while (preg_match('/\\s/isSu', $this->peek()) === 1) { $this->consume(1); } if ($this->oParserSettings->bLenientParsing) { try { $oComment = $this->consumeComment(); } catch (UnexpectedEOFException $e) { $this->iCurrentPosition = $this->iLength; return $aComments; } } else { $oComment = $this->consumeComment(); } if ($oComment !== false) { $aComments[] = $oComment; } } while ($oComment !== false); return $aComments; } /** * @param string $sString * @param bool $bCaseInsensitive * * @return bool */ public function comes($sString, $bCaseInsensitive = false) { $sPeek = $this->peek(strlen($sString)); return ($sPeek == '') ? false : $this->streql($sPeek, $sString, $bCaseInsensitive); } /** * @param int $iLength * @param int $iOffset * * @return string */ public function peek($iLength = 1, $iOffset = 0) { $iOffset += $this->iCurrentPosition; if ($iOffset >= $this->iLength) { return ''; } return $this->substr($iOffset, $iLength); } /** * @param int $mValue * * @return string * * @throws UnexpectedEOFException * @throws UnexpectedTokenException */ public function consume($mValue = 1) { if (is_string($mValue)) { $iLineCount = substr_count($mValue, "\n"); $iLength = $this->strlen($mValue); if (!$this->streql($this->substr($this->iCurrentPosition, $iLength), $mValue)) { throw new UnexpectedTokenException($mValue, $this->peek(max($iLength, 5)), $this->iLineNo); } $this->iLineNo += $iLineCount; $this->iCurrentPosition += $this->strlen($mValue); return $mValue; } else { if ($this->iCurrentPosition + $mValue > $this->iLength) { throw new UnexpectedEOFException($mValue, $this->peek(5), 'count', $this->iLineNo); } $sResult = $this->substr($this->iCurrentPosition, $mValue); $iLineCount = substr_count($sResult, "\n"); $this->iLineNo += $iLineCount; $this->iCurrentPosition += $mValue; return $sResult; } } /** * @param string $mExpression * @param int|null $iMaxLength * * @return string * * @throws UnexpectedEOFException * @throws UnexpectedTokenException */ public function consumeExpression($mExpression, $iMaxLength = null) { $aMatches = null; $sInput = $iMaxLength !== null ? $this->peek($iMaxLength) : $this->inputLeft(); if (preg_match($mExpression, $sInput, $aMatches, PREG_OFFSET_CAPTURE) === 1) { return $this->consume($aMatches[0][0]); } throw new UnexpectedTokenException($mExpression, $this->peek(5), 'expression', $this->iLineNo); } /** * @return Comment|false */ public function consumeComment() { $mComment = false; if ($this->comes('/*')) { $iLineNo = $this->iLineNo; $this->consume(1); $mComment = ''; while (($char = $this->consume(1)) !== '') { $mComment .= $char; if ($this->comes('*/')) { $this->consume(2); break; } } } if ($mComment !== false) { // We skip the * which was included in the comment. return new Comment(substr($mComment, 1), $iLineNo); } return $mComment; } /** * @return bool */ public function isEnd() { return $this->iCurrentPosition >= $this->iLength; } /** * @param array<array-key, string>|string $aEnd * @param string $bIncludeEnd * @param string $consumeEnd * @param array<int, Comment> $comments * * @return string * * @throws UnexpectedEOFException * @throws UnexpectedTokenException */ public function consumeUntil($aEnd, $bIncludeEnd = false, $consumeEnd = false, array &$comments = []) { $aEnd = is_array($aEnd) ? $aEnd : [$aEnd]; $out = ''; $start = $this->iCurrentPosition; while (!$this->isEnd()) { $char = $this->consume(1); if (in_array($char, $aEnd)) { if ($bIncludeEnd) { $out .= $char; } elseif (!$consumeEnd) { $this->iCurrentPosition -= $this->strlen($char); } return $out; } $out .= $char; if ($comment = $this->consumeComment()) { $comments[] = $comment; } } if (in_array(self::EOF, $aEnd)) { return $out; } $this->iCurrentPosition = $start; throw new UnexpectedEOFException( 'One of ("' . implode('","', $aEnd) . '")', $this->peek(5), 'search', $this->iLineNo ); } /** * @return string */ private function inputLeft() { return $this->substr($this->iCurrentPosition, -1); } /** * @param string $sString1 * @param string $sString2 * @param bool $bCaseInsensitive * * @return bool */ public function streql($sString1, $sString2, $bCaseInsensitive = true) { if ($bCaseInsensitive) { return $this->strtolower($sString1) === $this->strtolower($sString2); } else { return $sString1 === $sString2; } } /** * @param int $iAmount * * @return void */ public function backtrack($iAmount) { $this->iCurrentPosition -= $iAmount; } /** * @param string $sString * * @return int */ public function strlen($sString) { if ($this->oParserSettings->bMultibyteSupport) { return mb_strlen($sString, $this->sCharset); } else { return strlen($sString); } } /** * @param int $iStart * @param int $iLength * * @return string */ private function substr($iStart, $iLength) { if ($iLength < 0) { $iLength = $this->iLength - $iStart + $iLength; } if ($iStart + $iLength > $this->iLength) { $iLength = $this->iLength - $iStart; } $sResult = ''; while ($iLength > 0) { $sResult .= $this->aText[$iStart]; $iStart++; $iLength--; } return $sResult; } /** * @param string $sString * * @return string */ private function strtolower($sString) { if ($this->oParserSettings->bMultibyteSupport) { return mb_strtolower($sString, $this->sCharset); } else { return strtolower($sString); } } /** * @param string $sString * * @return array<int, string> */ private function strsplit($sString) { if ($this->oParserSettings->bMultibyteSupport) { if ($this->streql($this->sCharset, 'utf-8')) { return preg_split('//u', $sString, -1, PREG_SPLIT_NO_EMPTY); } else { $iLength = mb_strlen($sString, $this->sCharset); $aResult = []; for ($i = 0; $i < $iLength; ++$i) { $aResult[] = mb_substr($sString, $i, 1, $this->sCharset); } return $aResult; } } else { if ($sString === '') { return []; } else { return str_split($sString); } } } /** * @param string $sString * @param string $sNeedle * @param int $iOffset * * @return int|false */ private function strpos($sString, $sNeedle, $iOffset) { if ($this->oParserSettings->bMultibyteSupport) { return mb_strpos($sString, $sNeedle, $iOffset, $this->sCharset); } else { return strpos($sString, $sNeedle, $iOffset); } } } packages/Sabberworm/CSS/Parsing/SourceException.php 0000644 00000001145 15217646631 0016324 0 ustar 00 <?php namespace Automattic\WooCommerce\Vendor\Sabberworm\CSS\Parsing; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Position\Position; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Position\Positionable; class SourceException extends \Exception implements Positionable { use Position; /** * @param string $sMessage * @param int $iLineNo */ public function __construct($sMessage, $iLineNo = 0) { $this->setPosition($iLineNo); if (!empty($iLineNo)) { $sMessage .= " [line no: $iLineNo]"; } parent::__construct($sMessage); } } packages/Sabberworm/CSS/Parsing/OutputException.php 0000644 00000000604 15217646631 0016363 0 ustar 00 <?php namespace Automattic\WooCommerce\Vendor\Sabberworm\CSS\Parsing; /** * Thrown if the CSS parser attempts to print something invalid. */ class OutputException extends SourceException { /** * @param string $sMessage * @param int $iLineNo */ public function __construct($sMessage, $iLineNo = 0) { parent::__construct($sMessage, $iLineNo); } } packages/Sabberworm/CSS/Parsing/UnexpectedTokenException.php 0000644 00000002742 15217646631 0020175 0 ustar 00 <?php namespace Automattic\WooCommerce\Vendor\Sabberworm\CSS\Parsing; /** * Thrown if the CSS parser encounters a token it did not expect. */ class UnexpectedTokenException extends SourceException { /** * @var string */ private $sExpected; /** * @var string */ private $sFound; /** * Possible values: literal, identifier, count, expression, search * * @var string */ private $sMatchType; /** * @param string $sExpected * @param string $sFound * @param string $sMatchType * @param int $iLineNo */ public function __construct($sExpected, $sFound, $sMatchType = 'literal', $iLineNo = 0) { $this->sExpected = $sExpected; $this->sFound = $sFound; $this->sMatchType = $sMatchType; $sMessage = "Token “{$sExpected}” ({$sMatchType}) not found. Got “{$sFound}”."; if ($this->sMatchType === 'search') { $sMessage = "Search for “{$sExpected}” returned no results. Context: “{$sFound}”."; } elseif ($this->sMatchType === 'count') { $sMessage = "Next token was expected to have {$sExpected} chars. Context: “{$sFound}”."; } elseif ($this->sMatchType === 'identifier') { $sMessage = "Identifier expected. Got “{$sFound}”"; } elseif ($this->sMatchType === 'custom') { $sMessage = trim("$sExpected $sFound"); } parent::__construct($sMessage, $iLineNo); } } packages/Sabberworm/CSS/Parsing/UnexpectedEOFException.php 0000644 00000000457 15217646632 0017530 0 ustar 00 <?php namespace Automattic\WooCommerce\Vendor\Sabberworm\CSS\Parsing; /** * Thrown if the CSS parser encounters end of file it did not expect. * * Extends `UnexpectedTokenException` in order to preserve backwards compatibility. */ class UnexpectedEOFException extends UnexpectedTokenException { } packages/Sabberworm/CSS/Parsing/Anchor.php 0000644 00000001354 15217646632 0014422 0 ustar 00 <?php namespace Automattic\WooCommerce\Vendor\Sabberworm\CSS\Parsing; /** * @internal since 8.7.0 */ class Anchor { /** * @var int */ private $iPosition; /** * @var \Automattic\WooCommerce\Vendor\Sabberworm\CSS\Parsing\ParserState */ private $oParserState; /** * @param int $iPosition * @param \Automattic\WooCommerce\Vendor\Sabberworm\CSS\Parsing\ParserState $oParserState */ public function __construct($iPosition, ParserState $oParserState) { $this->iPosition = $iPosition; $this->oParserState = $oParserState; } /** * @return void */ public function backtrack() { $this->oParserState->setPosition($this->iPosition); } } packages/Sabberworm/CSS/Comment/Comment.php 0000644 00000002622 15217646632 0014610 0 ustar 00 <?php namespace Automattic\WooCommerce\Vendor\Sabberworm\CSS\Comment; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\OutputFormat; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Renderable; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Position\Position; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Position\Positionable; class Comment implements Positionable, Renderable { use Position; /** * @var string * * @internal since 8.8.0 */ protected $sComment; /** * @param string $sComment * @param int $iLineNo */ public function __construct($sComment = '', $iLineNo = 0) { $this->sComment = $sComment; $this->setPosition($iLineNo); } /** * @return string */ public function getComment() { return $this->sComment; } /** * @param string $sComment * * @return void */ public function setComment($sComment) { $this->sComment = $sComment; } /** * @return string * * @deprecated in V8.8.0, will be removed in V9.0.0. Use `render` instead. */ public function __toString() { return $this->render(new OutputFormat()); } /** * @param OutputFormat|null $oOutputFormat * * @return string */ public function render($oOutputFormat) { return '/*' . $this->sComment . '*/'; } } packages/Sabberworm/CSS/Comment/Commentable.php 0000644 00000000742 15217646632 0015435 0 ustar 00 <?php namespace Automattic\WooCommerce\Vendor\Sabberworm\CSS\Comment; interface Commentable { /** * @param array<array-key, Comment> $aComments * * @return void */ public function addComments(array $aComments); /** * @return array<array-key, Comment> */ public function getComments(); /** * @param array<array-key, Comment> $aComments * * @return void */ public function setComments(array $aComments); } packages/Sabberworm/CSS/Renderable.php 0000644 00000000705 15217646632 0013647 0 ustar 00 <?php namespace Automattic\WooCommerce\Vendor\Sabberworm\CSS; interface Renderable { /** * @return string * * @deprecated in V8.8.0, will be removed in V9.0.0. Use `render` instead. */ public function __toString(); /** * @param OutputFormat|null $oOutputFormat * * @return string */ public function render($oOutputFormat); /** * @return int */ public function getLineNo(); } packages/Sabberworm/CSS/Rule/Rule.php 0000644 00000025325 15217646632 0013427 0 ustar 00 <?php namespace Automattic\WooCommerce\Vendor\Sabberworm\CSS\Rule; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Comment\Comment; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Comment\Commentable; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\CSSElement; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\OutputFormat; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Parsing\ParserState; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Parsing\UnexpectedEOFException; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Parsing\UnexpectedTokenException; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Position\Position; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Position\Positionable; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Value\RuleValueList; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Value\Value; /** * `Rule`s just have a string key (the rule) and a 'Value'. * * In CSS, `Rule`s are expressed as follows: “key: value[0][0] value[0][1], value[1][0] value[1][1];” */ class Rule implements Commentable, CSSElement, Positionable { use Position; /** * @var string */ private $sRule; /** * @var RuleValueList|string|null */ private $mValue; /** * @var bool */ private $bIsImportant; /** * @var array<int, int> */ private $aIeHack; /** * @var array<array-key, Comment> * * @internal since 8.8.0 */ protected $aComments; /** * @param string $sRule * @param int $iLineNo * @param int $iColNo */ public function __construct($sRule, $iLineNo = 0, $iColNo = 0) { $this->sRule = $sRule; $this->mValue = null; $this->bIsImportant = false; $this->aIeHack = []; $this->setPosition($iLineNo, $iColNo); $this->aComments = []; } /** * @param array<int, Comment> $commentsBeforeRule * * @return Rule * * @throws UnexpectedEOFException * @throws UnexpectedTokenException * * @internal since V8.8.0 */ public static function parse(ParserState $oParserState, $commentsBeforeRule = []) { $aComments = \array_merge($commentsBeforeRule, $oParserState->consumeWhiteSpace()); $oRule = new Rule( $oParserState->parseIdentifier(!$oParserState->comes("--")), $oParserState->currentLine(), $oParserState->currentColumn() ); $oRule->setComments($aComments); $oRule->addComments($oParserState->consumeWhiteSpace()); $oParserState->consume(':'); $oValue = Value::parseValue($oParserState, self::listDelimiterForRule($oRule->getRule())); $oRule->setValue($oValue); if ($oParserState->getSettings()->bLenientParsing) { while ($oParserState->comes('\\')) { $oParserState->consume('\\'); $oRule->addIeHack($oParserState->consume()); $oParserState->consumeWhiteSpace(); } } $oParserState->consumeWhiteSpace(); if ($oParserState->comes('!')) { $oParserState->consume('!'); $oParserState->consumeWhiteSpace(); $oParserState->consume('important'); $oRule->setIsImportant(true); } $oParserState->consumeWhiteSpace(); while ($oParserState->comes(';')) { $oParserState->consume(';'); } return $oRule; } /** * Returns a list of delimiters (or separators). * The first item is the innermost separator (or, put another way, the highest-precedence operator). * The sequence continues to the outermost separator (or lowest-precedence operator). * * @param string $sRule * * @return list<non-empty-string> */ private static function listDelimiterForRule($sRule) { if (preg_match('/^font($|-)/', $sRule)) { return [',', '/', ' ']; } switch ($sRule) { case 'src': return [' ', ',']; default: return [',', ' ', '/']; } } /** * @param string $sRule * * @return void */ public function setRule($sRule) { $this->sRule = $sRule; } /** * @return string */ public function getRule() { return $this->sRule; } /** * @return RuleValueList|string|null */ public function getValue() { return $this->mValue; } /** * @param RuleValueList|string|null $mValue * * @return void */ public function setValue($mValue) { $this->mValue = $mValue; } /** * @param array<array-key, array<array-key, RuleValueList>> $aSpaceSeparatedValues * * @return RuleValueList * * @deprecated will be removed in version 9.0 * Old-Style 2-dimensional array given. Retained for (some) backwards-compatibility. * Use `setValue()` instead and wrap the value inside a RuleValueList if necessary. */ public function setValues(array $aSpaceSeparatedValues) { $oSpaceSeparatedList = null; if (count($aSpaceSeparatedValues) > 1) { $oSpaceSeparatedList = new RuleValueList(' ', $this->iLineNo); } foreach ($aSpaceSeparatedValues as $aCommaSeparatedValues) { $oCommaSeparatedList = null; if (count($aCommaSeparatedValues) > 1) { $oCommaSeparatedList = new RuleValueList(',', $this->iLineNo); } foreach ($aCommaSeparatedValues as $mValue) { if (!$oSpaceSeparatedList && !$oCommaSeparatedList) { $this->mValue = $mValue; return $mValue; } if ($oCommaSeparatedList) { $oCommaSeparatedList->addListComponent($mValue); } else { $oSpaceSeparatedList->addListComponent($mValue); } } if (!$oSpaceSeparatedList) { $this->mValue = $oCommaSeparatedList; return $oCommaSeparatedList; } else { $oSpaceSeparatedList->addListComponent($oCommaSeparatedList); } } $this->mValue = $oSpaceSeparatedList; return $oSpaceSeparatedList; } /** * @return array<int, array<int, RuleValueList>> * * @deprecated will be removed in version 9.0 * Old-Style 2-dimensional array returned. Retained for (some) backwards-compatibility. * Use `getValue()` instead and check for the existence of a (nested set of) ValueList object(s). */ public function getValues() { if (!$this->mValue instanceof RuleValueList) { return [[$this->mValue]]; } if ($this->mValue->getListSeparator() === ',') { return [$this->mValue->getListComponents()]; } $aResult = []; foreach ($this->mValue->getListComponents() as $mValue) { if (!$mValue instanceof RuleValueList || $mValue->getListSeparator() !== ',') { $aResult[] = [$mValue]; continue; } if ($this->mValue->getListSeparator() === ' ' || count($aResult) === 0) { $aResult[] = []; } foreach ($mValue->getListComponents() as $mValue) { $aResult[count($aResult) - 1][] = $mValue; } } return $aResult; } /** * Adds a value to the existing value. Value will be appended if a `RuleValueList` exists of the given type. * Otherwise, the existing value will be wrapped by one. * * @param RuleValueList|array<int, RuleValueList> $mValue * @param string $sType * * @return void */ public function addValue($mValue, $sType = ' ') { if (!is_array($mValue)) { $mValue = [$mValue]; } if (!$this->mValue instanceof RuleValueList || $this->mValue->getListSeparator() !== $sType) { $mCurrentValue = $this->mValue; $this->mValue = new RuleValueList($sType, $this->getLineNumber()); if ($mCurrentValue) { $this->mValue->addListComponent($mCurrentValue); } } foreach ($mValue as $mValueItem) { $this->mValue->addListComponent($mValueItem); } } /** * @param int $iModifier * * @return void * * @deprecated since V8.8.0, will be removed in V9.0 */ public function addIeHack($iModifier) { $this->aIeHack[] = $iModifier; } /** * @param array<int, int> $aModifiers * * @return void * * @deprecated since V8.8.0, will be removed in V9.0 */ public function setIeHack(array $aModifiers) { $this->aIeHack = $aModifiers; } /** * @return array<int, int> * * @deprecated since V8.8.0, will be removed in V9.0 */ public function getIeHack() { return $this->aIeHack; } /** * @param bool $bIsImportant * * @return void */ public function setIsImportant($bIsImportant) { $this->bIsImportant = $bIsImportant; } /** * @return bool */ public function getIsImportant() { return $this->bIsImportant; } /** * @return string * * @deprecated in V8.8.0, will be removed in V9.0.0. Use `render` instead. */ public function __toString() { return $this->render(new OutputFormat()); } /** * @param OutputFormat|null $oOutputFormat * * @return string */ public function render($oOutputFormat) { $sResult = "{$oOutputFormat->comments($this)}{$this->sRule}:{$oOutputFormat->spaceAfterRuleName()}"; if ($this->mValue instanceof Value) { // Can also be a ValueList $sResult .= $this->mValue->render($oOutputFormat); } else { $sResult .= $this->mValue; } if (!empty($this->aIeHack)) { $sResult .= ' \\' . implode('\\', $this->aIeHack); } if ($this->bIsImportant) { $sResult .= ' !important'; } $sResult .= ';'; return $sResult; } /** * @param array<array-key, Comment> $aComments * * @return void */ public function addComments(array $aComments) { $this->aComments = array_merge($this->aComments, $aComments); } /** * @return array<array-key, Comment> */ public function getComments() { return $this->aComments; } /** * @param array<array-key, Comment> $aComments * * @return void */ public function setComments(array $aComments) { $this->aComments = $aComments; } } packages/Sabberworm/CSS/Property/KeyframeSelector.php 0000644 00000001373 15217646632 0016676 0 ustar 00 <?php namespace Automattic\WooCommerce\Vendor\Sabberworm\CSS\Property; class KeyframeSelector extends Selector { /** * regexp for specificity calculations * * @var string * * @internal since 8.5.2 */ const SELECTOR_VALIDATION_RX = '/ ^( (?: [a-zA-Z0-9\x{00A0}-\x{FFFF}_^$|*="\'~\[\]()\-\s\.:#+>]* # any sequence of valid unescaped characters (?:\\\\.)? # a single escaped character (?:([\'"]).*?(?<!\\\\)\2)? # a quoted text like [id="example"] )* )| (\d+%) # keyframe animation progress percentage (e.g. 50%) $ /ux'; } packages/Sabberworm/CSS/Property/AtRule.php 0000644 00000001703 15217646632 0014623 0 ustar 00 <?php namespace Automattic\WooCommerce\Vendor\Sabberworm\CSS\Property; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Comment\Commentable; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Renderable; interface AtRule extends Renderable, Commentable { /** * Since there are more set rules than block rules, * we’re whitelisting the block rules and have anything else be treated as a set rule. * * @var string * * @internal since 8.5.2 */ const BLOCK_RULES = 'media/document/supports/region-style/font-feature-values'; /** * … and more font-specific ones (to be used inside font-feature-values) * * @var string * * @internal since 8.5.2 */ const SET_RULES = 'font-face/counter-style/page/swash/styleset/annotation'; /** * @return string|null */ public function atRuleName(); /** * @return string|null */ public function atRuleArgs(); } packages/Sabberworm/CSS/Property/CSSNamespace.php 0000644 00000005745 15217646632 0015706 0 ustar 00 <?php namespace Automattic\WooCommerce\Vendor\Sabberworm\CSS\Property; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Comment\Comment; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\OutputFormat; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Position\Position; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Position\Positionable; /** * `CSSNamespace` represents an `@namespace` rule. */ class CSSNamespace implements AtRule, Positionable { use Position; /** * @var string */ private $mUrl; /** * @var string */ private $sPrefix; /** * @var int */ private $iLineNo; /** * @var array<array-key, Comment> * * @internal since 8.8.0 */ protected $aComments; /** * @param string $mUrl * @param string|null $sPrefix * @param int $iLineNo */ public function __construct($mUrl, $sPrefix = null, $iLineNo = 0) { $this->mUrl = $mUrl; $this->sPrefix = $sPrefix; $this->setPosition($iLineNo); $this->aComments = []; } /** * @return string * * @deprecated in V8.8.0, will be removed in V9.0.0. Use `render` instead. */ public function __toString() { return $this->render(new OutputFormat()); } /** * @param OutputFormat|null $oOutputFormat * * @return string */ public function render($oOutputFormat) { return '@namespace ' . ($this->sPrefix === null ? '' : $this->sPrefix . ' ') . $this->mUrl->render($oOutputFormat) . ';'; } /** * @return string */ public function getUrl() { return $this->mUrl; } /** * @return string|null */ public function getPrefix() { return $this->sPrefix; } /** * @param string $mUrl * * @return void */ public function setUrl($mUrl) { $this->mUrl = $mUrl; } /** * @param string $sPrefix * * @return void */ public function setPrefix($sPrefix) { $this->sPrefix = $sPrefix; } /** * @return string */ public function atRuleName() { return 'namespace'; } /** * @return array<int, string> */ public function atRuleArgs() { $aResult = [$this->mUrl]; if ($this->sPrefix) { array_unshift($aResult, $this->sPrefix); } return $aResult; } /** * @param array<array-key, Comment> $aComments * * @return void */ public function addComments(array $aComments) { $this->aComments = array_merge($this->aComments, $aComments); } /** * @return array<array-key, Comment> */ public function getComments() { return $this->aComments; } /** * @param array<array-key, Comment> $aComments * * @return void */ public function setComments(array $aComments) { $this->aComments = $aComments; } } packages/Sabberworm/CSS/Property/Charset.php 0000644 00000005547 15217646633 0015033 0 ustar 00 <?php namespace Automattic\WooCommerce\Vendor\Sabberworm\CSS\Property; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Comment\Comment; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\OutputFormat; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Position\Position; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Position\Positionable; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Value\CSSString; /** * Class representing an `@charset` rule. * * The following restrictions apply: * - May not be found in any CSSList other than the Document. * - May only appear at the very top of a Document’s contents. * - Must not appear more than once. */ class Charset implements AtRule, Positionable { use Position; /** * @var CSSString */ private $oCharset; /** * @var int * * @internal since 8.8.0 */ protected $iLineNo; /** * @var array<array-key, Comment> * * @internal since 8.8.0 */ protected $aComments; /** * @param CSSString $oCharset * @param int $iLineNo */ public function __construct(CSSString $oCharset, $iLineNo = 0) { $this->oCharset = $oCharset; $this->setPosition($iLineNo); $this->aComments = []; } /** * @param string|CSSString $oCharset * * @return void */ public function setCharset($sCharset) { $sCharset = $sCharset instanceof CSSString ? $sCharset : new CSSString($sCharset); $this->oCharset = $sCharset; } /** * @return string */ public function getCharset() { return $this->oCharset->getString(); } /** * @return string * * @deprecated in V8.8.0, will be removed in V9.0.0. Use `render` instead. */ public function __toString() { return $this->render(new OutputFormat()); } /** * @param OutputFormat|null $oOutputFormat * * @return string */ public function render($oOutputFormat) { return "{$oOutputFormat->comments($this)}@charset {$this->oCharset->render($oOutputFormat)};"; } /** * @return string */ public function atRuleName() { return 'charset'; } /** * @return string */ public function atRuleArgs() { return $this->oCharset; } /** * @param array<array-key, Comment> $aComments * * @return void */ public function addComments(array $aComments) { $this->aComments = array_merge($this->aComments, $aComments); } /** * @return array<array-key, Comment> */ public function getComments() { return $this->aComments; } /** * @param array<array-key, Comment> $aComments * * @return void */ public function setComments(array $aComments) { $this->aComments = $aComments; } } packages/Sabberworm/CSS/Property/Selector.php 0000644 00000007214 15217646633 0015213 0 ustar 00 <?php namespace Automattic\WooCommerce\Vendor\Sabberworm\CSS\Property; /** * Class representing a single CSS selector. Selectors have to be split by the comma prior to being passed into this * class. */ class Selector { /** * regexp for specificity calculations * * @var string * * @internal */ const NON_ID_ATTRIBUTES_AND_PSEUDO_CLASSES_RX = '/ (\.[\w]+) # classes | \[(\w+) # attributes | (\:( # pseudo classes link|visited|active |hover|focus |lang |target |enabled|disabled|checked|indeterminate |root |nth-child|nth-last-child|nth-of-type|nth-last-of-type |first-child|last-child|first-of-type|last-of-type |only-child|only-of-type |empty|contains )) /ix'; /** * regexp for specificity calculations * * @var string * * @internal */ const ELEMENTS_AND_PSEUDO_ELEMENTS_RX = '/ ((^|[\s\+\>\~]+)[\w]+ # elements | \:{1,2}( # pseudo-elements after|before|first-letter|first-line|selection )) /ix'; /** * regexp for specificity calculations * * @var string * * @internal since 8.5.2 */ const SELECTOR_VALIDATION_RX = '/ ^( (?: [a-zA-Z0-9\x{00A0}-\x{FFFF}_^$|*="\'~\[\]()\-\s\.:#+>]* # any sequence of valid unescaped characters (?:\\\\.)? # a single escaped character (?:([\'"]).*?(?<!\\\\)\2)? # a quoted text like [id="example"] )* )$ /ux'; /** * @var string */ private $sSelector; /** * @var int|null */ private $iSpecificity; /** * @param string $sSelector * * @return bool * * @internal since V8.8.0 */ public static function isValid($sSelector) { return preg_match(static::SELECTOR_VALIDATION_RX, $sSelector); } /** * @param string $sSelector * @param bool $bCalculateSpecificity @deprecated since V8.8.0, will be removed in V9.0.0 */ public function __construct($sSelector, $bCalculateSpecificity = false) { $this->setSelector($sSelector); if ($bCalculateSpecificity) { $this->getSpecificity(); } } /** * @return string */ public function getSelector() { return $this->sSelector; } /** * @param string $sSelector * * @return void */ public function setSelector($sSelector) { $this->sSelector = trim($sSelector); $this->iSpecificity = null; } /** * @return string * * @deprecated in V8.8.0, will be removed in V9.0.0. Use `render` instead. */ public function __toString() { return $this->getSelector(); } /** * @return int */ public function getSpecificity() { if ($this->iSpecificity === null) { $a = 0; /// @todo should exclude \# as well as "#" $aMatches = null; $b = substr_count($this->sSelector, '#'); $c = preg_match_all(self::NON_ID_ATTRIBUTES_AND_PSEUDO_CLASSES_RX, $this->sSelector, $aMatches); $d = preg_match_all(self::ELEMENTS_AND_PSEUDO_ELEMENTS_RX, $this->sSelector, $aMatches); $this->iSpecificity = ($a * 1000) + ($b * 100) + ($c * 10) + $d; } return $this->iSpecificity; } } packages/Sabberworm/CSS/Property/Import.php 0000644 00000005657 15217646633 0014716 0 ustar 00 <?php namespace Automattic\WooCommerce\Vendor\Sabberworm\CSS\Property; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Comment\Comment; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\OutputFormat; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Position\Position; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Position\Positionable; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Value\URL; /** * Class representing an `@import` rule. */ class Import implements AtRule, Positionable { use Position; /** * @var URL */ private $oLocation; /** * @var string */ private $sMediaQuery; /** * @var array<array-key, Comment> * * @internal since 8.8.0 */ protected $aComments; /** * @param URL $oLocation * @param string $sMediaQuery * @param int $iLineNo */ public function __construct(URL $oLocation, $sMediaQuery, $iLineNo = 0) { $this->oLocation = $oLocation; $this->sMediaQuery = $sMediaQuery; $this->setPosition($iLineNo); $this->aComments = []; } /** * @param URL $oLocation * * @return void */ public function setLocation($oLocation) { $this->oLocation = $oLocation; } /** * @return URL */ public function getLocation() { return $this->oLocation; } /** * @return string * * @deprecated in V8.8.0, will be removed in V9.0.0. Use `render` instead. */ public function __toString() { return $this->render(new OutputFormat()); } /** * @param OutputFormat|null $oOutputFormat * * @return string */ public function render($oOutputFormat) { return $oOutputFormat->comments($this) . "@import " . $this->oLocation->render($oOutputFormat) . ($this->sMediaQuery === null ? '' : ' ' . $this->sMediaQuery) . ';'; } /** * @return string */ public function atRuleName() { return 'import'; } /** * @return array<int, URL|string> */ public function atRuleArgs() { $aResult = [$this->oLocation]; if ($this->sMediaQuery) { array_push($aResult, $this->sMediaQuery); } return $aResult; } /** * @param array<array-key, Comment> $aComments * * @return void */ public function addComments(array $aComments) { $this->aComments = array_merge($this->aComments, $aComments); } /** * @return array<array-key, Comment> */ public function getComments() { return $this->aComments; } /** * @param array<array-key, Comment> $aComments * * @return void */ public function setComments(array $aComments) { $this->aComments = $aComments; } /** * @return string */ public function getMediaQuery() { return $this->sMediaQuery; } } packages/Sabberworm/CSS/Parser.php 0000644 00000003650 15217646633 0013043 0 ustar 00 <?php namespace Automattic\WooCommerce\Vendor\Sabberworm\CSS; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\CSSList\Document; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Parsing\ParserState; use Automattic\WooCommerce\Vendor\Sabberworm\CSS\Parsing\SourceException; /** * This class parses CSS from text into a data structure. */ class Parser { /** * @var ParserState */ private $oParserState; /** * @param string $sText the complete CSS as text (i.e., usually the contents of a CSS file) * @param Settings|null $oParserSettings * @param int $iLineNo the line number (starting from 1, not from 0) */ public function __construct($sText, $oParserSettings = null, $iLineNo = 1) { if ($oParserSettings === null) { $oParserSettings = Settings::create(); } $this->oParserState = new ParserState($sText, $oParserSettings, $iLineNo); } /** * Sets the charset to be used if the CSS does not contain an `@charset` declaration. * * @param string $sCharset * * @return void * * @deprecated since 8.7.0, will be removed in version 9.0.0 with #687 */ public function setCharset($sCharset) { $this->oParserState->setCharset($sCharset); } /** * Returns the charset that is used if the CSS does not contain an `@charset` declaration. * * @return void * * @deprecated since 8.7.0, will be removed in version 9.0.0 with #687 */ public function getCharset() { // Note: The `return` statement is missing here. This is a bug that needs to be fixed. $this->oParserState->getCharset(); } /** * Parses the CSS provided to the constructor and creates a `Document` from it. * * @return Document * * @throws SourceException */ public function parse() { return Document::parse($this->oParserState); } } packages/League/ISO3166/ISO3166DataValidator.php 0000644 00000003446 15217646633 0014645 0 ustar 00 <?php declare(strict_types=1); /* * (c) Rob Bast <rob.bast@gmail.com> * * For the full copyright and license information, please view * the LICENSE file that was distributed with this source code. */ namespace Automattic\WooCommerce\Vendor\League\ISO3166; use Automattic\WooCommerce\Vendor\League\ISO3166\Exception\DomainException; final class ISO3166DataValidator { /** * @param array<array<string, mixed>> $data * * @return array<array<string, mixed>> */ public function validate(array $data): array { foreach ($data as $entry) { $this->assertEntryHasRequiredKeys($entry); } return $data; } /** * @param array<string, mixed> $entry * * @throws \Automattic\WooCommerce\Vendor\League\ISO3166\Exception\DomainException if given data entry does not have all the required keys */ private function assertEntryHasRequiredKeys(array $entry): void { if (!isset($entry[ISO3166::KEY_NAME])) { throw new DomainException('Each data entry must have a name key.'); } Guards::guardAgainstInvalidName($entry[ISO3166::KEY_NAME]); if (!isset($entry[ISO3166::KEY_ALPHA2])) { throw new DomainException('Each data entry must have a alpha2 key.'); } Guards::guardAgainstInvalidAlpha2($entry[ISO3166::KEY_ALPHA2]); if (!isset($entry[ISO3166::KEY_ALPHA3])) { throw new DomainException('Each data entry must have a alpha3 key.'); } Guards::guardAgainstInvalidAlpha3($entry[ISO3166::KEY_ALPHA3]); if (!isset($entry[ISO3166::KEY_NUMERIC])) { throw new DomainException('Each data entry must have a numeric key.'); } Guards::guardAgainstInvalidNumeric($entry[ISO3166::KEY_NUMERIC]); } } packages/League/ISO3166/ISO3166.php 0000644 00000166500 15217646633 0012206 0 ustar 00 <?php declare(strict_types=1); /* * (c) Rob Bast <rob.bast@gmail.com> * * For the full copyright and license information, please view * the LICENSE file that was distributed with this source code. */ namespace Automattic\WooCommerce\Vendor\League\ISO3166; use Automattic\WooCommerce\Vendor\League\ISO3166\Exception\DomainException; use Automattic\WooCommerce\Vendor\League\ISO3166\Exception\OutOfBoundsException; /** @implements \IteratorAggregate<string, array> */ final class ISO3166 implements \Countable, \IteratorAggregate, ISO3166DataProvider { /** @var string */ public const KEY_ALPHA2 = 'alpha2'; /** @var string */ public const KEY_ALPHA3 = 'alpha3'; /** @var string */ public const KEY_NUMERIC = 'numeric'; /** @var string */ public const KEY_NAME = 'name'; /** @var string[] */ private array $keys = [self::KEY_ALPHA2, self::KEY_ALPHA3, self::KEY_NUMERIC, self::KEY_NAME]; /** * @param array<array{name: string, alpha2: string, alpha3: string, numeric: numeric-string, currency: string[]}> $countries replace default dataset with given array */ public function __construct(array $countries = []) { if ([] !== $countries) { $this->countries = $countries; } } /** * @return array{name: string, alpha2: string, alpha3: string, numeric: numeric-string, currency: string[]} */ public function name(string $name): array { Guards::guardAgainstInvalidName($name); return $this->lookup(self::KEY_NAME, $name); } /** * @return array{name: string, alpha2: string, alpha3: string, numeric: numeric-string, currency: string[]} */ public function alpha2(string $alpha2): array { Guards::guardAgainstInvalidAlpha2($alpha2); return $this->lookup(self::KEY_ALPHA2, $alpha2); } /** * @return array{name: string, alpha2: string, alpha3: string, numeric: numeric-string, currency: string[]} */ public function alpha3(string $alpha3): array { Guards::guardAgainstInvalidAlpha3($alpha3); return $this->lookup(self::KEY_ALPHA3, $alpha3); } /** * @return array{name: string, alpha2: string, alpha3: string, numeric: numeric-string, currency: string[]} */ public function numeric(string $numeric): array { Guards::guardAgainstInvalidNumeric($numeric); return $this->lookup(self::KEY_NUMERIC, $numeric); } /** * @return array{name: string, alpha2: string, alpha3: string, numeric: numeric-string, currency: string[]} */ public function exactName(string $name): array { Guards::guardAgainstInvalidName($name); $value = mb_strtolower($name); foreach ($this->countries as $country) { $comparison = mb_strtolower($country[self::KEY_NAME]); if ($value === $comparison) { return $country; } } throw new OutOfBoundsException(sprintf('No "%s" key found matching: %s', self::KEY_NAME, $value)); } /** * @return array<array{name: string, alpha2: string, alpha3: string, numeric: numeric-string, currency: string[]}> */ public function all(): array { return $this->countries; } /** * @param 'name'|'alpha2'|'alpha3'|'numeric' $key * * @throws \Automattic\WooCommerce\Vendor\League\ISO3166\Exception\DomainException if an invalid key is specified * * @return \Generator<string, array{name: string, alpha2: string, alpha3: string, numeric: numeric-string, currency: string[]}> */ public function iterator(string $key = self::KEY_ALPHA2): \Generator { if (!in_array($key, $this->keys, true)) { throw new DomainException(sprintf('Invalid value for $key, got "%s", expected one of: %s', $key, implode(', ', $this->keys))); } foreach ($this->countries as $country) { yield $country[$key] => $country; } } /** * @see \Countable * * @internal */ public function count(): int { return count($this->countries); } /** * @return \Generator<array<string, string|array<string>>> * * @see \IteratorAggregate * * @internal */ public function getIterator(): \Generator { foreach ($this->countries as $country) { yield $country; } } /** * Lookup ISO3166-1 data by given identifier. * * Looks for a match against the given key for each entry in the dataset. * * @param 'name'|'alpha2'|'alpha3'|'numeric' $key * * @throws \Automattic\WooCommerce\Vendor\League\ISO3166\Exception\OutOfBoundsException if key does not exist in dataset * * @return array{name: string, alpha2: string, alpha3: string, numeric: numeric-string, currency: string[]} */ private function lookup(string $key, string $value): array { $value = mb_strtolower($value); foreach ($this->countries as $country) { $comparison = mb_strtolower($country[$key]); if ($value === $comparison || $value === mb_substr($comparison, 0, mb_strlen($value))) { return $country; } } throw new OutOfBoundsException(sprintf('No "%s" key found matching: %s', $key, $value)); } /** * Default dataset. * * @var array<array{name: string, alpha2: string, alpha3: string, numeric: numeric-string, currency: string[]}>> */ private array $countries = [ [ 'name' => 'Afghanistan', 'alpha2' => 'AF', 'alpha3' => 'AFG', 'numeric' => '004', 'currency' => [ 'AFN', ], ], [ 'name' => 'Åland Islands', 'alpha2' => 'AX', 'alpha3' => 'ALA', 'numeric' => '248', 'currency' => [ 'EUR', ], ], [ 'name' => 'Albania', 'alpha2' => 'AL', 'alpha3' => 'ALB', 'numeric' => '008', 'currency' => [ 'ALL', ], ], [ 'name' => 'Algeria', 'alpha2' => 'DZ', 'alpha3' => 'DZA', 'numeric' => '012', 'currency' => [ 'DZD', ], ], [ 'name' => 'American Samoa', 'alpha2' => 'AS', 'alpha3' => 'ASM', 'numeric' => '016', 'currency' => [ 'USD', ], ], [ 'name' => 'Andorra', 'alpha2' => 'AD', 'alpha3' => 'AND', 'numeric' => '020', 'currency' => [ 'EUR', ], ], [ 'name' => 'Angola', 'alpha2' => 'AO', 'alpha3' => 'AGO', 'numeric' => '024', 'currency' => [ 'AOA', ], ], [ 'name' => 'Anguilla', 'alpha2' => 'AI', 'alpha3' => 'AIA', 'numeric' => '660', 'currency' => [ 'XCD', ], ], [ 'name' => 'Antarctica', 'alpha2' => 'AQ', 'alpha3' => 'ATA', 'numeric' => '010', 'currency' => [ 'ARS', 'AUD', 'BGN', 'BRL', 'BYR', 'CLP', 'CNY', 'CZK', 'EUR', 'GBP', 'INR', 'JPY', 'KRW', 'NOK', 'NZD', 'PEN', 'PKR', 'PLN', 'RON', 'RUB', 'SEK', 'UAH', 'USD', 'UYU', 'ZAR', ], ], [ 'name' => 'Antigua and Barbuda', 'alpha2' => 'AG', 'alpha3' => 'ATG', 'numeric' => '028', 'currency' => [ 'XCD', ], ], [ 'name' => 'Argentina', 'alpha2' => 'AR', 'alpha3' => 'ARG', 'numeric' => '032', 'currency' => [ 'ARS', ], ], [ 'name' => 'Armenia', 'alpha2' => 'AM', 'alpha3' => 'ARM', 'numeric' => '051', 'currency' => [ 'AMD', ], ], [ 'name' => 'Aruba', 'alpha2' => 'AW', 'alpha3' => 'ABW', 'numeric' => '533', 'currency' => [ 'AWG', ], ], [ 'name' => 'Australia', 'alpha2' => 'AU', 'alpha3' => 'AUS', 'numeric' => '036', 'currency' => [ 'AUD', ], ], [ 'name' => 'Austria', 'alpha2' => 'AT', 'alpha3' => 'AUT', 'numeric' => '040', 'currency' => [ 'EUR', ], ], [ 'name' => 'Azerbaijan', 'alpha2' => 'AZ', 'alpha3' => 'AZE', 'numeric' => '031', 'currency' => [ 'AZN', ], ], [ 'name' => 'Bahamas', 'alpha2' => 'BS', 'alpha3' => 'BHS', 'numeric' => '044', 'currency' => [ 'BSD', ], ], [ 'name' => 'Bahrain', 'alpha2' => 'BH', 'alpha3' => 'BHR', 'numeric' => '048', 'currency' => [ 'BHD', ], ], [ 'name' => 'Bangladesh', 'alpha2' => 'BD', 'alpha3' => 'BGD', 'numeric' => '050', 'currency' => [ 'BDT', ], ], [ 'name' => 'Barbados', 'alpha2' => 'BB', 'alpha3' => 'BRB', 'numeric' => '052', 'currency' => [ 'BBD', ], ], [ 'name' => 'Belarus', 'alpha2' => 'BY', 'alpha3' => 'BLR', 'numeric' => '112', 'currency' => [ 'BYN', ], ], [ 'name' => 'Belgium', 'alpha2' => 'BE', 'alpha3' => 'BEL', 'numeric' => '056', 'currency' => [ 'EUR', ], ], [ 'name' => 'Belize', 'alpha2' => 'BZ', 'alpha3' => 'BLZ', 'numeric' => '084', 'currency' => [ 'BZD', ], ], [ 'name' => 'Benin', 'alpha2' => 'BJ', 'alpha3' => 'BEN', 'numeric' => '204', 'currency' => [ 'XOF', ], ], [ 'name' => 'Bermuda', 'alpha2' => 'BM', 'alpha3' => 'BMU', 'numeric' => '060', 'currency' => [ 'BMD', ], ], [ 'name' => 'Bhutan', 'alpha2' => 'BT', 'alpha3' => 'BTN', 'numeric' => '064', 'currency' => [ 'BTN', ], ], [ 'name' => 'Bolivia (Plurinational State of)', 'alpha2' => 'BO', 'alpha3' => 'BOL', 'numeric' => '068', 'currency' => [ 'BOB', ], ], [ 'name' => 'Bonaire, Sint Eustatius and Saba', 'alpha2' => 'BQ', 'alpha3' => 'BES', 'numeric' => '535', 'currency' => [ 'USD', ], ], [ 'name' => 'Bosnia and Herzegovina', 'alpha2' => 'BA', 'alpha3' => 'BIH', 'numeric' => '070', 'currency' => [ 'BAM', ], ], [ 'name' => 'Botswana', 'alpha2' => 'BW', 'alpha3' => 'BWA', 'numeric' => '072', 'currency' => [ 'BWP', ], ], [ 'name' => 'Bouvet Island', 'alpha2' => 'BV', 'alpha3' => 'BVT', 'numeric' => '074', 'currency' => [ 'NOK', ], ], [ 'name' => 'Brazil', 'alpha2' => 'BR', 'alpha3' => 'BRA', 'numeric' => '076', 'currency' => [ 'BRL', ], ], [ 'name' => 'British Indian Ocean Territory', 'alpha2' => 'IO', 'alpha3' => 'IOT', 'numeric' => '086', 'currency' => [ 'GBP', ], ], [ 'name' => 'Brunei Darussalam', 'alpha2' => 'BN', 'alpha3' => 'BRN', 'numeric' => '096', 'currency' => [ 'BND', 'SGD', ], ], [ 'name' => 'Bulgaria', 'alpha2' => 'BG', 'alpha3' => 'BGR', 'numeric' => '100', 'currency' => [ 'BGN', ], ], [ 'name' => 'Burkina Faso', 'alpha2' => 'BF', 'alpha3' => 'BFA', 'numeric' => '854', 'currency' => [ 'XOF', ], ], [ 'name' => 'Burundi', 'alpha2' => 'BI', 'alpha3' => 'BDI', 'numeric' => '108', 'currency' => [ 'BIF', ], ], [ 'name' => 'Cabo Verde', 'alpha2' => 'CV', 'alpha3' => 'CPV', 'numeric' => '132', 'currency' => [ 'CVE', ], ], [ 'name' => 'Cambodia', 'alpha2' => 'KH', 'alpha3' => 'KHM', 'numeric' => '116', 'currency' => [ 'KHR', ], ], [ 'name' => 'Cameroon', 'alpha2' => 'CM', 'alpha3' => 'CMR', 'numeric' => '120', 'currency' => [ 'XAF', ], ], [ 'name' => 'Canada', 'alpha2' => 'CA', 'alpha3' => 'CAN', 'numeric' => '124', 'currency' => [ 'CAD', ], ], [ 'name' => 'Cayman Islands', 'alpha2' => 'KY', 'alpha3' => 'CYM', 'numeric' => '136', 'currency' => [ 'KYD', ], ], [ 'name' => 'Central African Republic', 'alpha2' => 'CF', 'alpha3' => 'CAF', 'numeric' => '140', 'currency' => [ 'XAF', ], ], [ 'name' => 'Chad', 'alpha2' => 'TD', 'alpha3' => 'TCD', 'numeric' => '148', 'currency' => [ 'XAF', ], ], [ 'name' => 'Chile', 'alpha2' => 'CL', 'alpha3' => 'CHL', 'numeric' => '152', 'currency' => [ 'CLP', ], ], [ 'name' => 'China', 'alpha2' => 'CN', 'alpha3' => 'CHN', 'numeric' => '156', 'currency' => [ 'CNY', ], ], [ 'name' => 'Christmas Island', 'alpha2' => 'CX', 'alpha3' => 'CXR', 'numeric' => '162', 'currency' => [ 'AUD', ], ], [ 'name' => 'Cocos (Keeling) Islands', 'alpha2' => 'CC', 'alpha3' => 'CCK', 'numeric' => '166', 'currency' => [ 'AUD', ], ], [ 'name' => 'Colombia', 'alpha2' => 'CO', 'alpha3' => 'COL', 'numeric' => '170', 'currency' => [ 'COP', ], ], [ 'name' => 'Comoros', 'alpha2' => 'KM', 'alpha3' => 'COM', 'numeric' => '174', 'currency' => [ 'KMF', ], ], [ 'name' => 'Congo', 'alpha2' => 'CG', 'alpha3' => 'COG', 'numeric' => '178', 'currency' => [ 'XAF', ], ], [ 'name' => 'Congo (Democratic Republic of the)', 'alpha2' => 'CD', 'alpha3' => 'COD', 'numeric' => '180', 'currency' => [ 'CDF', ], ], [ 'name' => 'Cook Islands', 'alpha2' => 'CK', 'alpha3' => 'COK', 'numeric' => '184', 'currency' => [ 'NZD', ], ], [ 'name' => 'Costa Rica', 'alpha2' => 'CR', 'alpha3' => 'CRI', 'numeric' => '188', 'currency' => [ 'CRC', ], ], [ 'name' => 'Côte d\'Ivoire', 'alpha2' => 'CI', 'alpha3' => 'CIV', 'numeric' => '384', 'currency' => [ 'XOF', ], ], [ 'name' => 'Croatia', 'alpha2' => 'HR', 'alpha3' => 'HRV', 'numeric' => '191', 'currency' => [ 'EUR', ], ], [ 'name' => 'Cuba', 'alpha2' => 'CU', 'alpha3' => 'CUB', 'numeric' => '192', 'currency' => [ 'CUC', 'CUP', ], ], [ 'name' => 'Curaçao', 'alpha2' => 'CW', 'alpha3' => 'CUW', 'numeric' => '531', 'currency' => [ 'ANG', ], ], [ 'name' => 'Cyprus', 'alpha2' => 'CY', 'alpha3' => 'CYP', 'numeric' => '196', 'currency' => [ 'EUR', ], ], [ 'name' => 'Czechia', 'alpha2' => 'CZ', 'alpha3' => 'CZE', 'numeric' => '203', 'currency' => [ 'CZK', ], ], [ 'name' => 'Denmark', 'alpha2' => 'DK', 'alpha3' => 'DNK', 'numeric' => '208', 'currency' => [ 'DKK', ], ], [ 'name' => 'Djibouti', 'alpha2' => 'DJ', 'alpha3' => 'DJI', 'numeric' => '262', 'currency' => [ 'DJF', ], ], [ 'name' => 'Dominica', 'alpha2' => 'DM', 'alpha3' => 'DMA', 'numeric' => '212', 'currency' => [ 'XCD', ], ], [ 'name' => 'Dominican Republic', 'alpha2' => 'DO', 'alpha3' => 'DOM', 'numeric' => '214', 'currency' => [ 'DOP', ], ], [ 'name' => 'Ecuador', 'alpha2' => 'EC', 'alpha3' => 'ECU', 'numeric' => '218', 'currency' => [ 'USD', ], ], [ 'name' => 'Egypt', 'alpha2' => 'EG', 'alpha3' => 'EGY', 'numeric' => '818', 'currency' => [ 'EGP', ], ], [ 'name' => 'El Salvador', 'alpha2' => 'SV', 'alpha3' => 'SLV', 'numeric' => '222', 'currency' => [ 'USD', ], ], [ 'name' => 'Equatorial Guinea', 'alpha2' => 'GQ', 'alpha3' => 'GNQ', 'numeric' => '226', 'currency' => [ 'XAF', ], ], [ 'name' => 'Eritrea', 'alpha2' => 'ER', 'alpha3' => 'ERI', 'numeric' => '232', 'currency' => [ 'ERN', ], ], [ 'name' => 'Estonia', 'alpha2' => 'EE', 'alpha3' => 'EST', 'numeric' => '233', 'currency' => [ 'EUR', ], ], [ 'name' => 'Ethiopia', 'alpha2' => 'ET', 'alpha3' => 'ETH', 'numeric' => '231', 'currency' => [ 'ETB', ], ], [ 'name' => 'Eswatini', 'alpha2' => 'SZ', 'alpha3' => 'SWZ', 'numeric' => '748', 'currency' => [ 'SZL', 'ZAR', ], ], [ 'name' => 'Falkland Islands (Malvinas)', 'alpha2' => 'FK', 'alpha3' => 'FLK', 'numeric' => '238', 'currency' => [ 'FKP', ], ], [ 'name' => 'Faroe Islands', 'alpha2' => 'FO', 'alpha3' => 'FRO', 'numeric' => '234', 'currency' => [ 'DKK', ], ], [ 'name' => 'Fiji', 'alpha2' => 'FJ', 'alpha3' => 'FJI', 'numeric' => '242', 'currency' => [ 'FJD', ], ], [ 'name' => 'Finland', 'alpha2' => 'FI', 'alpha3' => 'FIN', 'numeric' => '246', 'currency' => [ 'EUR', ], ], [ 'name' => 'France', 'alpha2' => 'FR', 'alpha3' => 'FRA', 'numeric' => '250', 'currency' => [ 'EUR', ], ], [ 'name' => 'French Guiana', 'alpha2' => 'GF', 'alpha3' => 'GUF', 'numeric' => '254', 'currency' => [ 'EUR', ], ], [ 'name' => 'French Polynesia', 'alpha2' => 'PF', 'alpha3' => 'PYF', 'numeric' => '258', 'currency' => [ 'XPF', ], ], [ 'name' => 'French Southern Territories', 'alpha2' => 'TF', 'alpha3' => 'ATF', 'numeric' => '260', 'currency' => [ 'EUR', ], ], [ 'name' => 'Gabon', 'alpha2' => 'GA', 'alpha3' => 'GAB', 'numeric' => '266', 'currency' => [ 'XAF', ], ], [ 'name' => 'Gambia', 'alpha2' => 'GM', 'alpha3' => 'GMB', 'numeric' => '270', 'currency' => [ 'GMD', ], ], [ 'name' => 'Georgia', 'alpha2' => 'GE', 'alpha3' => 'GEO', 'numeric' => '268', 'currency' => [ 'GEL', ], ], [ 'name' => 'Germany', 'alpha2' => 'DE', 'alpha3' => 'DEU', 'numeric' => '276', 'currency' => [ 'EUR', ], ], [ 'name' => 'Ghana', 'alpha2' => 'GH', 'alpha3' => 'GHA', 'numeric' => '288', 'currency' => [ 'GHS', ], ], [ 'name' => 'Gibraltar', 'alpha2' => 'GI', 'alpha3' => 'GIB', 'numeric' => '292', 'currency' => [ 'GIP', ], ], [ 'name' => 'Greece', 'alpha2' => 'GR', 'alpha3' => 'GRC', 'numeric' => '300', 'currency' => [ 'EUR', ], ], [ 'name' => 'Greenland', 'alpha2' => 'GL', 'alpha3' => 'GRL', 'numeric' => '304', 'currency' => [ 'DKK', ], ], [ 'name' => 'Grenada', 'alpha2' => 'GD', 'alpha3' => 'GRD', 'numeric' => '308', 'currency' => [ 'XCD', ], ], [ 'name' => 'Guadeloupe', 'alpha2' => 'GP', 'alpha3' => 'GLP', 'numeric' => '312', 'currency' => [ 'EUR', ], ], [ 'name' => 'Guam', 'alpha2' => 'GU', 'alpha3' => 'GUM', 'numeric' => '316', 'currency' => [ 'USD', ], ], [ 'name' => 'Guatemala', 'alpha2' => 'GT', 'alpha3' => 'GTM', 'numeric' => '320', 'currency' => [ 'GTQ', ], ], [ 'name' => 'Guernsey', 'alpha2' => 'GG', 'alpha3' => 'GGY', 'numeric' => '831', 'currency' => [ 'GBP', ], ], [ 'name' => 'Guinea', 'alpha2' => 'GN', 'alpha3' => 'GIN', 'numeric' => '324', 'currency' => [ 'GNF', ], ], [ 'name' => 'Guinea-Bissau', 'alpha2' => 'GW', 'alpha3' => 'GNB', 'numeric' => '624', 'currency' => [ 'XOF', ], ], [ 'name' => 'Guyana', 'alpha2' => 'GY', 'alpha3' => 'GUY', 'numeric' => '328', 'currency' => [ 'GYD', ], ], [ 'name' => 'Haiti', 'alpha2' => 'HT', 'alpha3' => 'HTI', 'numeric' => '332', 'currency' => [ 'HTG', ], ], [ 'name' => 'Heard Island and McDonald Islands', 'alpha2' => 'HM', 'alpha3' => 'HMD', 'numeric' => '334', 'currency' => [ 'AUD', ], ], [ 'name' => 'Holy See', 'alpha2' => 'VA', 'alpha3' => 'VAT', 'numeric' => '336', 'currency' => [ 'EUR', ], ], [ 'name' => 'Honduras', 'alpha2' => 'HN', 'alpha3' => 'HND', 'numeric' => '340', 'currency' => [ 'HNL', ], ], [ 'name' => 'Hong Kong', 'alpha2' => 'HK', 'alpha3' => 'HKG', 'numeric' => '344', 'currency' => [ 'HKD', ], ], [ 'name' => 'Hungary', 'alpha2' => 'HU', 'alpha3' => 'HUN', 'numeric' => '348', 'currency' => [ 'HUF', ], ], [ 'name' => 'Iceland', 'alpha2' => 'IS', 'alpha3' => 'ISL', 'numeric' => '352', 'currency' => [ 'ISK', ], ], [ 'name' => 'India', 'alpha2' => 'IN', 'alpha3' => 'IND', 'numeric' => '356', 'currency' => [ 'INR', ], ], [ 'name' => 'Indonesia', 'alpha2' => 'ID', 'alpha3' => 'IDN', 'numeric' => '360', 'currency' => [ 'IDR', ], ], [ 'name' => 'Iran (Islamic Republic of)', 'alpha2' => 'IR', 'alpha3' => 'IRN', 'numeric' => '364', 'currency' => [ 'IRR', ], ], [ 'name' => 'Iraq', 'alpha2' => 'IQ', 'alpha3' => 'IRQ', 'numeric' => '368', 'currency' => [ 'IQD', ], ], [ 'name' => 'Ireland', 'alpha2' => 'IE', 'alpha3' => 'IRL', 'numeric' => '372', 'currency' => [ 'EUR', ], ], [ 'name' => 'Isle of Man', 'alpha2' => 'IM', 'alpha3' => 'IMN', 'numeric' => '833', 'currency' => [ 'GBP', ], ], [ 'name' => 'Israel', 'alpha2' => 'IL', 'alpha3' => 'ISR', 'numeric' => '376', 'currency' => [ 'ILS', ], ], [ 'name' => 'Italy', 'alpha2' => 'IT', 'alpha3' => 'ITA', 'numeric' => '380', 'currency' => [ 'EUR', ], ], [ 'name' => 'Jamaica', 'alpha2' => 'JM', 'alpha3' => 'JAM', 'numeric' => '388', 'currency' => [ 'JMD', ], ], [ 'name' => 'Japan', 'alpha2' => 'JP', 'alpha3' => 'JPN', 'numeric' => '392', 'currency' => [ 'JPY', ], ], [ 'name' => 'Jersey', 'alpha2' => 'JE', 'alpha3' => 'JEY', 'numeric' => '832', 'currency' => [ 'GBP', ], ], [ 'name' => 'Jordan', 'alpha2' => 'JO', 'alpha3' => 'JOR', 'numeric' => '400', 'currency' => [ 'JOD', ], ], [ 'name' => 'Kazakhstan', 'alpha2' => 'KZ', 'alpha3' => 'KAZ', 'numeric' => '398', 'currency' => [ 'KZT', ], ], [ 'name' => 'Kenya', 'alpha2' => 'KE', 'alpha3' => 'KEN', 'numeric' => '404', 'currency' => [ 'KES', ], ], [ 'name' => 'Kiribati', 'alpha2' => 'KI', 'alpha3' => 'KIR', 'numeric' => '296', 'currency' => [ 'AUD', ], ], [ 'name' => 'Korea (Democratic People\'s Republic of)', 'alpha2' => 'KP', 'alpha3' => 'PRK', 'numeric' => '408', 'currency' => [ 'KPW', ], ], [ 'name' => 'Korea (Republic of)', 'alpha2' => 'KR', 'alpha3' => 'KOR', 'numeric' => '410', 'currency' => [ 'KRW', ], ], [ 'name' => 'Kosovo', 'alpha2' => 'XK', 'alpha3' => 'XKX', 'numeric' => '412', 'currency' => [ 'EUR', ], ], [ 'name' => 'Kuwait', 'alpha2' => 'KW', 'alpha3' => 'KWT', 'numeric' => '414', 'currency' => [ 'KWD', ], ], [ 'name' => 'Kyrgyzstan', 'alpha2' => 'KG', 'alpha3' => 'KGZ', 'numeric' => '417', 'currency' => [ 'KGS', ], ], [ 'name' => 'Lao People\'s Democratic Republic', 'alpha2' => 'LA', 'alpha3' => 'LAO', 'numeric' => '418', 'currency' => [ 'LAK', ], ], [ 'name' => 'Latvia', 'alpha2' => 'LV', 'alpha3' => 'LVA', 'numeric' => '428', 'currency' => [ 'EUR', ], ], [ 'name' => 'Lebanon', 'alpha2' => 'LB', 'alpha3' => 'LBN', 'numeric' => '422', 'currency' => [ 'LBP', ], ], [ 'name' => 'Lesotho', 'alpha2' => 'LS', 'alpha3' => 'LSO', 'numeric' => '426', 'currency' => [ 'LSL', 'ZAR', ], ], [ 'name' => 'Liberia', 'alpha2' => 'LR', 'alpha3' => 'LBR', 'numeric' => '430', 'currency' => [ 'LRD', ], ], [ 'name' => 'Libya', 'alpha2' => 'LY', 'alpha3' => 'LBY', 'numeric' => '434', 'currency' => [ 'LYD', ], ], [ 'name' => 'Liechtenstein', 'alpha2' => 'LI', 'alpha3' => 'LIE', 'numeric' => '438', 'currency' => [ 'CHF', ], ], [ 'name' => 'Lithuania', 'alpha2' => 'LT', 'alpha3' => 'LTU', 'numeric' => '440', 'currency' => [ 'EUR', ], ], [ 'name' => 'Luxembourg', 'alpha2' => 'LU', 'alpha3' => 'LUX', 'numeric' => '442', 'currency' => [ 'EUR', ], ], [ 'name' => 'Macao', 'alpha2' => 'MO', 'alpha3' => 'MAC', 'numeric' => '446', 'currency' => [ 'MOP', ], ], [ 'name' => 'North Macedonia', 'alpha2' => 'MK', 'alpha3' => 'MKD', 'numeric' => '807', 'currency' => [ 'MKD', ], ], [ 'name' => 'Madagascar', 'alpha2' => 'MG', 'alpha3' => 'MDG', 'numeric' => '450', 'currency' => [ 'MGA', ], ], [ 'name' => 'Malawi', 'alpha2' => 'MW', 'alpha3' => 'MWI', 'numeric' => '454', 'currency' => [ 'MWK', ], ], [ 'name' => 'Malaysia', 'alpha2' => 'MY', 'alpha3' => 'MYS', 'numeric' => '458', 'currency' => [ 'MYR', ], ], [ 'name' => 'Maldives', 'alpha2' => 'MV', 'alpha3' => 'MDV', 'numeric' => '462', 'currency' => [ 'MVR', ], ], [ 'name' => 'Mali', 'alpha2' => 'ML', 'alpha3' => 'MLI', 'numeric' => '466', 'currency' => [ 'XOF', ], ], [ 'name' => 'Malta', 'alpha2' => 'MT', 'alpha3' => 'MLT', 'numeric' => '470', 'currency' => [ 'EUR', ], ], [ 'name' => 'Marshall Islands', 'alpha2' => 'MH', 'alpha3' => 'MHL', 'numeric' => '584', 'currency' => [ 'USD', ], ], [ 'name' => 'Martinique', 'alpha2' => 'MQ', 'alpha3' => 'MTQ', 'numeric' => '474', 'currency' => [ 'EUR', ], ], [ 'name' => 'Mauritania', 'alpha2' => 'MR', 'alpha3' => 'MRT', 'numeric' => '478', 'currency' => [ 'MRO', ], ], [ 'name' => 'Mauritius', 'alpha2' => 'MU', 'alpha3' => 'MUS', 'numeric' => '480', 'currency' => [ 'MUR', ], ], [ 'name' => 'Mayotte', 'alpha2' => 'YT', 'alpha3' => 'MYT', 'numeric' => '175', 'currency' => [ 'EUR', ], ], [ 'name' => 'Mexico', 'alpha2' => 'MX', 'alpha3' => 'MEX', 'numeric' => '484', 'currency' => [ 'MXN', ], ], [ 'name' => 'Micronesia (Federated States of)', 'alpha2' => 'FM', 'alpha3' => 'FSM', 'numeric' => '583', 'currency' => [ 'USD', ], ], [ 'name' => 'Moldova (Republic of)', 'alpha2' => 'MD', 'alpha3' => 'MDA', 'numeric' => '498', 'currency' => [ 'MDL', ], ], [ 'name' => 'Monaco', 'alpha2' => 'MC', 'alpha3' => 'MCO', 'numeric' => '492', 'currency' => [ 'EUR', ], ], [ 'name' => 'Mongolia', 'alpha2' => 'MN', 'alpha3' => 'MNG', 'numeric' => '496', 'currency' => [ 'MNT', ], ], [ 'name' => 'Montenegro', 'alpha2' => 'ME', 'alpha3' => 'MNE', 'numeric' => '499', 'currency' => [ 'EUR', ], ], [ 'name' => 'Montserrat', 'alpha2' => 'MS', 'alpha3' => 'MSR', 'numeric' => '500', 'currency' => [ 'XCD', ], ], [ 'name' => 'Morocco', 'alpha2' => 'MA', 'alpha3' => 'MAR', 'numeric' => '504', 'currency' => [ 'MAD', ], ], [ 'name' => 'Mozambique', 'alpha2' => 'MZ', 'alpha3' => 'MOZ', 'numeric' => '508', 'currency' => [ 'MZN', ], ], [ 'name' => 'Myanmar', 'alpha2' => 'MM', 'alpha3' => 'MMR', 'numeric' => '104', 'currency' => [ 'MMK', ], ], [ 'name' => 'Namibia', 'alpha2' => 'NA', 'alpha3' => 'NAM', 'numeric' => '516', 'currency' => [ 'NAD', 'ZAR', ], ], [ 'name' => 'Nauru', 'alpha2' => 'NR', 'alpha3' => 'NRU', 'numeric' => '520', 'currency' => [ 'AUD', ], ], [ 'name' => 'Nepal', 'alpha2' => 'NP', 'alpha3' => 'NPL', 'numeric' => '524', 'currency' => [ 'NPR', ], ], [ 'name' => 'Netherlands', 'alpha2' => 'NL', 'alpha3' => 'NLD', 'numeric' => '528', 'currency' => [ 'EUR', ], ], [ 'name' => 'New Caledonia', 'alpha2' => 'NC', 'alpha3' => 'NCL', 'numeric' => '540', 'currency' => [ 'XPF', ], ], [ 'name' => 'New Zealand', 'alpha2' => 'NZ', 'alpha3' => 'NZL', 'numeric' => '554', 'currency' => [ 'NZD', ], ], [ 'name' => 'Nicaragua', 'alpha2' => 'NI', 'alpha3' => 'NIC', 'numeric' => '558', 'currency' => [ 'NIO', ], ], [ 'name' => 'Niger', 'alpha2' => 'NE', 'alpha3' => 'NER', 'numeric' => '562', 'currency' => [ 'XOF', ], ], [ 'name' => 'Nigeria', 'alpha2' => 'NG', 'alpha3' => 'NGA', 'numeric' => '566', 'currency' => [ 'NGN', ], ], [ 'name' => 'Niue', 'alpha2' => 'NU', 'alpha3' => 'NIU', 'numeric' => '570', 'currency' => [ 'NZD', ], ], [ 'name' => 'Norfolk Island', 'alpha2' => 'NF', 'alpha3' => 'NFK', 'numeric' => '574', 'currency' => [ 'AUD', ], ], [ 'name' => 'Northern Mariana Islands', 'alpha2' => 'MP', 'alpha3' => 'MNP', 'numeric' => '580', 'currency' => [ 'USD', ], ], [ 'name' => 'Norway', 'alpha2' => 'NO', 'alpha3' => 'NOR', 'numeric' => '578', 'currency' => [ 'NOK', ], ], [ 'name' => 'Oman', 'alpha2' => 'OM', 'alpha3' => 'OMN', 'numeric' => '512', 'currency' => [ 'OMR', ], ], [ 'name' => 'Pakistan', 'alpha2' => 'PK', 'alpha3' => 'PAK', 'numeric' => '586', 'currency' => [ 'PKR', ], ], [ 'name' => 'Palau', 'alpha2' => 'PW', 'alpha3' => 'PLW', 'numeric' => '585', 'currency' => [ 'USD', ], ], [ 'name' => 'Palestine, State of', 'alpha2' => 'PS', 'alpha3' => 'PSE', 'numeric' => '275', 'currency' => [ 'ILS', ], ], [ 'name' => 'Panama', 'alpha2' => 'PA', 'alpha3' => 'PAN', 'numeric' => '591', 'currency' => [ 'PAB', ], ], [ 'name' => 'Papua New Guinea', 'alpha2' => 'PG', 'alpha3' => 'PNG', 'numeric' => '598', 'currency' => [ 'PGK', ], ], [ 'name' => 'Paraguay', 'alpha2' => 'PY', 'alpha3' => 'PRY', 'numeric' => '600', 'currency' => [ 'PYG', ], ], [ 'name' => 'Peru', 'alpha2' => 'PE', 'alpha3' => 'PER', 'numeric' => '604', 'currency' => [ 'PEN', ], ], [ 'name' => 'Philippines', 'alpha2' => 'PH', 'alpha3' => 'PHL', 'numeric' => '608', 'currency' => [ 'PHP', ], ], [ 'name' => 'Pitcairn', 'alpha2' => 'PN', 'alpha3' => 'PCN', 'numeric' => '612', 'currency' => [ 'NZD', ], ], [ 'name' => 'Poland', 'alpha2' => 'PL', 'alpha3' => 'POL', 'numeric' => '616', 'currency' => [ 'PLN', ], ], [ 'name' => 'Portugal', 'alpha2' => 'PT', 'alpha3' => 'PRT', 'numeric' => '620', 'currency' => [ 'EUR', ], ], [ 'name' => 'Puerto Rico', 'alpha2' => 'PR', 'alpha3' => 'PRI', 'numeric' => '630', 'currency' => [ 'USD', ], ], [ 'name' => 'Qatar', 'alpha2' => 'QA', 'alpha3' => 'QAT', 'numeric' => '634', 'currency' => [ 'QAR', ], ], [ 'name' => 'Réunion', 'alpha2' => 'RE', 'alpha3' => 'REU', 'numeric' => '638', 'currency' => [ 'EUR', ], ], [ 'name' => 'Romania', 'alpha2' => 'RO', 'alpha3' => 'ROU', 'numeric' => '642', 'currency' => [ 'RON', ], ], [ 'name' => 'Russian Federation', 'alpha2' => 'RU', 'alpha3' => 'RUS', 'numeric' => '643', 'currency' => [ 'RUB', ], ], [ 'name' => 'Rwanda', 'alpha2' => 'RW', 'alpha3' => 'RWA', 'numeric' => '646', 'currency' => [ 'RWF', ], ], [ 'name' => 'Saint Barthélemy', 'alpha2' => 'BL', 'alpha3' => 'BLM', 'numeric' => '652', 'currency' => [ 'EUR', ], ], [ 'name' => 'Saint Helena, Ascension and Tristan da Cunha', 'alpha2' => 'SH', 'alpha3' => 'SHN', 'numeric' => '654', 'currency' => [ 'SHP', ], ], [ 'name' => 'Saint Kitts and Nevis', 'alpha2' => 'KN', 'alpha3' => 'KNA', 'numeric' => '659', 'currency' => [ 'XCD', ], ], [ 'name' => 'Saint Lucia', 'alpha2' => 'LC', 'alpha3' => 'LCA', 'numeric' => '662', 'currency' => [ 'XCD', ], ], [ 'name' => 'Saint Martin (French part)', 'alpha2' => 'MF', 'alpha3' => 'MAF', 'numeric' => '663', 'currency' => [ 'EUR', 'USD', ], ], [ 'name' => 'Saint Pierre and Miquelon', 'alpha2' => 'PM', 'alpha3' => 'SPM', 'numeric' => '666', 'currency' => [ 'EUR', ], ], [ 'name' => 'Saint Vincent and the Grenadines', 'alpha2' => 'VC', 'alpha3' => 'VCT', 'numeric' => '670', 'currency' => [ 'XCD', ], ], [ 'name' => 'Samoa', 'alpha2' => 'WS', 'alpha3' => 'WSM', 'numeric' => '882', 'currency' => [ 'WST', ], ], [ 'name' => 'San Marino', 'alpha2' => 'SM', 'alpha3' => 'SMR', 'numeric' => '674', 'currency' => [ 'EUR', ], ], [ 'name' => 'Sao Tome and Principe', 'alpha2' => 'ST', 'alpha3' => 'STP', 'numeric' => '678', 'currency' => [ 'STD', ], ], [ 'name' => 'Saudi Arabia', 'alpha2' => 'SA', 'alpha3' => 'SAU', 'numeric' => '682', 'currency' => [ 'SAR', ], ], [ 'name' => 'Senegal', 'alpha2' => 'SN', 'alpha3' => 'SEN', 'numeric' => '686', 'currency' => [ 'XOF', ], ], [ 'name' => 'Serbia', 'alpha2' => 'RS', 'alpha3' => 'SRB', 'numeric' => '688', 'currency' => [ 'RSD', ], ], [ 'name' => 'Seychelles', 'alpha2' => 'SC', 'alpha3' => 'SYC', 'numeric' => '690', 'currency' => [ 'SCR', ], ], [ 'name' => 'Sierra Leone', 'alpha2' => 'SL', 'alpha3' => 'SLE', 'numeric' => '694', 'currency' => [ 'SLL', ], ], [ 'name' => 'Singapore', 'alpha2' => 'SG', 'alpha3' => 'SGP', 'numeric' => '702', 'currency' => [ 'SGD', ], ], [ 'name' => 'Sint Maarten (Dutch part)', 'alpha2' => 'SX', 'alpha3' => 'SXM', 'numeric' => '534', 'currency' => [ 'ANG', ], ], [ 'name' => 'Slovakia', 'alpha2' => 'SK', 'alpha3' => 'SVK', 'numeric' => '703', 'currency' => [ 'EUR', ], ], [ 'name' => 'Slovenia', 'alpha2' => 'SI', 'alpha3' => 'SVN', 'numeric' => '705', 'currency' => [ 'EUR', ], ], [ 'name' => 'Solomon Islands', 'alpha2' => 'SB', 'alpha3' => 'SLB', 'numeric' => '090', 'currency' => [ 'SBD', ], ], [ 'name' => 'Somalia', 'alpha2' => 'SO', 'alpha3' => 'SOM', 'numeric' => '706', 'currency' => [ 'SOS', ], ], [ 'name' => 'South Africa', 'alpha2' => 'ZA', 'alpha3' => 'ZAF', 'numeric' => '710', 'currency' => [ 'ZAR', ], ], [ 'name' => 'South Georgia and the South Sandwich Islands', 'alpha2' => 'GS', 'alpha3' => 'SGS', 'numeric' => '239', 'currency' => [ 'GBP', ], ], [ 'name' => 'South Sudan', 'alpha2' => 'SS', 'alpha3' => 'SSD', 'numeric' => '728', 'currency' => [ 'SSP', ], ], [ 'name' => 'Spain', 'alpha2' => 'ES', 'alpha3' => 'ESP', 'numeric' => '724', 'currency' => [ 'EUR', ], ], [ 'name' => 'Sri Lanka', 'alpha2' => 'LK', 'alpha3' => 'LKA', 'numeric' => '144', 'currency' => [ 'LKR', ], ], [ 'name' => 'Sudan', 'alpha2' => 'SD', 'alpha3' => 'SDN', 'numeric' => '729', 'currency' => [ 'SDG', ], ], [ 'name' => 'Suriname', 'alpha2' => 'SR', 'alpha3' => 'SUR', 'numeric' => '740', 'currency' => [ 'SRD', ], ], [ 'name' => 'Svalbard and Jan Mayen', 'alpha2' => 'SJ', 'alpha3' => 'SJM', 'numeric' => '744', 'currency' => [ 'NOK', ], ], [ 'name' => 'Sweden', 'alpha2' => 'SE', 'alpha3' => 'SWE', 'numeric' => '752', 'currency' => [ 'SEK', ], ], [ 'name' => 'Switzerland', 'alpha2' => 'CH', 'alpha3' => 'CHE', 'numeric' => '756', 'currency' => [ 'CHF', ], ], [ 'name' => 'Syrian Arab Republic', 'alpha2' => 'SY', 'alpha3' => 'SYR', 'numeric' => '760', 'currency' => [ 'SYP', ], ], [ 'name' => 'Taiwan (Province of China)', 'alpha2' => 'TW', 'alpha3' => 'TWN', 'numeric' => '158', 'currency' => [ 'TWD', ], ], [ 'name' => 'Tajikistan', 'alpha2' => 'TJ', 'alpha3' => 'TJK', 'numeric' => '762', 'currency' => [ 'TJS', ], ], [ 'name' => 'Tanzania, United Republic of', 'alpha2' => 'TZ', 'alpha3' => 'TZA', 'numeric' => '834', 'currency' => [ 'TZS', ], ], [ 'name' => 'Thailand', 'alpha2' => 'TH', 'alpha3' => 'THA', 'numeric' => '764', 'currency' => [ 'THB', ], ], [ 'name' => 'Timor-Leste', 'alpha2' => 'TL', 'alpha3' => 'TLS', 'numeric' => '626', 'currency' => [ 'USD', ], ], [ 'name' => 'Togo', 'alpha2' => 'TG', 'alpha3' => 'TGO', 'numeric' => '768', 'currency' => [ 'XOF', ], ], [ 'name' => 'Tokelau', 'alpha2' => 'TK', 'alpha3' => 'TKL', 'numeric' => '772', 'currency' => [ 'NZD', ], ], [ 'name' => 'Tonga', 'alpha2' => 'TO', 'alpha3' => 'TON', 'numeric' => '776', 'currency' => [ 'TOP', ], ], [ 'name' => 'Trinidad and Tobago', 'alpha2' => 'TT', 'alpha3' => 'TTO', 'numeric' => '780', 'currency' => [ 'TTD', ], ], [ 'name' => 'Tunisia', 'alpha2' => 'TN', 'alpha3' => 'TUN', 'numeric' => '788', 'currency' => [ 'TND', ], ], [ 'name' => 'Türkiye', 'alpha2' => 'TR', 'alpha3' => 'TUR', 'numeric' => '792', 'currency' => [ 'TRY', ], ], [ 'name' => 'Turkmenistan', 'alpha2' => 'TM', 'alpha3' => 'TKM', 'numeric' => '795', 'currency' => [ 'TMT', ], ], [ 'name' => 'Turks and Caicos Islands', 'alpha2' => 'TC', 'alpha3' => 'TCA', 'numeric' => '796', 'currency' => [ 'USD', ], ], [ 'name' => 'Tuvalu', 'alpha2' => 'TV', 'alpha3' => 'TUV', 'numeric' => '798', 'currency' => [ 'AUD', ], ], [ 'name' => 'Uganda', 'alpha2' => 'UG', 'alpha3' => 'UGA', 'numeric' => '800', 'currency' => [ 'UGX', ], ], [ 'name' => 'Ukraine', 'alpha2' => 'UA', 'alpha3' => 'UKR', 'numeric' => '804', 'currency' => [ 'UAH', ], ], [ 'name' => 'United Arab Emirates', 'alpha2' => 'AE', 'alpha3' => 'ARE', 'numeric' => '784', 'currency' => [ 'AED', ], ], [ 'name' => 'United Kingdom of Great Britain and Northern Ireland', 'alpha2' => 'GB', 'alpha3' => 'GBR', 'numeric' => '826', 'currency' => [ 'GBP', ], ], [ 'name' => 'United States of America', 'alpha2' => 'US', 'alpha3' => 'USA', 'numeric' => '840', 'currency' => [ 'USD', ], ], [ 'name' => 'United States Minor Outlying Islands', 'alpha2' => 'UM', 'alpha3' => 'UMI', 'numeric' => '581', 'currency' => [ 'USD', ], ], [ 'name' => 'Uruguay', 'alpha2' => 'UY', 'alpha3' => 'URY', 'numeric' => '858', 'currency' => [ 'UYU', ], ], [ 'name' => 'Uzbekistan', 'alpha2' => 'UZ', 'alpha3' => 'UZB', 'numeric' => '860', 'currency' => [ 'UZS', ], ], [ 'name' => 'Vanuatu', 'alpha2' => 'VU', 'alpha3' => 'VUT', 'numeric' => '548', 'currency' => [ 'VUV', ], ], [ 'name' => 'Venezuela (Bolivarian Republic of)', 'alpha2' => 'VE', 'alpha3' => 'VEN', 'numeric' => '862', 'currency' => [ 'VEF', ], ], [ 'name' => 'Viet Nam', 'alpha2' => 'VN', 'alpha3' => 'VNM', 'numeric' => '704', 'currency' => [ 'VND', ], ], [ 'name' => 'Virgin Islands (British)', 'alpha2' => 'VG', 'alpha3' => 'VGB', 'numeric' => '092', 'currency' => [ 'USD', ], ], [ 'name' => 'Virgin Islands (U.S.)', 'alpha2' => 'VI', 'alpha3' => 'VIR', 'numeric' => '850', 'currency' => [ 'USD', ], ], [ 'name' => 'Wallis and Futuna', 'alpha2' => 'WF', 'alpha3' => 'WLF', 'numeric' => '876', 'currency' => [ 'XPF', ], ], [ 'name' => 'Western Sahara', 'alpha2' => 'EH', 'alpha3' => 'ESH', 'numeric' => '732', 'currency' => [ 'MAD', ], ], [ 'name' => 'Yemen', 'alpha2' => 'YE', 'alpha3' => 'YEM', 'numeric' => '887', 'currency' => [ 'YER', ], ], [ 'name' => 'Zambia', 'alpha2' => 'ZM', 'alpha3' => 'ZMB', 'numeric' => '894', 'currency' => [ 'ZMW', ], ], [ 'name' => 'Zimbabwe', 'alpha2' => 'ZW', 'alpha3' => 'ZWE', 'numeric' => '716', 'currency' => [ 'BWP', 'EUR', 'GBP', 'USD', 'ZAR', ], ], ]; } packages/League/ISO3166/ISO3166WithAliases.php 0000644 00000004555 15217646633 0014345 0 ustar 00 <?php declare(strict_types=1); /* * (c) Rob Bast <rob.bast@gmail.com> * * For the full copyright and license information, please view * the LICENSE file that was distributed with this source code. */ namespace Automattic\WooCommerce\Vendor\League\ISO3166; class ISO3166WithAliases implements ISO3166DataProvider { private ISO3166DataProvider $source; /** @type array<string, string> */ public const aliases = [ 'Bolivia' => 'Bolivia (Plurinational State of)', 'Bolivia, Plurinational State of' => 'Bolivia (Plurinational State of)', 'Congo-Kinshasa' => 'Congo (Democratic Republic of the)', 'Congo, Democratic Republic of the' => 'Congo (Democratic Republic of the)', 'Czech Republic' => 'Czechia', 'Iran' => 'Iran (Islamic Republic of)', 'North Korea' => 'Korea (Democratic People\'s Republic of)', 'South Korea' => 'Korea (Republic of)', 'Laos' => 'Lao People\'s Democratic Republic', 'Micronesia' => 'Micronesia (Federated States of)', 'Moldova' => 'Moldova (Republic of)', 'Palestine' => 'Palestine, State of', 'Russia' => 'Russian Federation', 'Saint Martin' => 'Saint Martin (French part)', 'Sint Maarten' => 'Sint Maarten (Dutch part)', 'Taiwan' => 'Taiwan (Province of China)', 'Tanzania' => 'Tanzania, United Republic of', 'United Kingdom' => 'United Kingdom of Great Britain and Northern Ireland', 'United States' => 'United States of America', 'USA' => 'United States of America', 'Venezuela' => 'Venezuela (Bolivarian Republic of)', 'Vietnam' => 'Viet Nam', ]; public function __construct(ISO3166DataProvider $iso3166) { $this->source = $iso3166; } public function name(string $name): array { foreach (self::aliases as $alias => $original) { if (0 === strcasecmp($alias, $name)) { $name = $original; break; } } return $this->source->name($name); } public function alpha2(string $alpha2): array { return $this->source->alpha2($alpha2); } public function alpha3(string $alpha3): array { return $this->source->alpha3($alpha3); } public function numeric(string $numeric): array { return $this->source->numeric($numeric); } } packages/League/ISO3166/Guards.php 0000644 00000004062 15217646633 0012453 0 ustar 00 <?php declare(strict_types=1); /* * (c) Rob Bast <rob.bast@gmail.com> * * For the full copyright and license information, please view * the LICENSE file that was distributed with this source code. */ namespace Automattic\WooCommerce\Vendor\League\ISO3166; use Automattic\WooCommerce\Vendor\League\ISO3166\Exception\DomainException; final class Guards { /** * Assert that input is not an empty string. * * @throws \Automattic\WooCommerce\Vendor\League\ISO3166\Exception\DomainException if input is an empty string */ public static function guardAgainstInvalidName(string $name): void { if ('' === trim($name)) { throw new DomainException('Expected string, got empty string'); } } /** * Assert that input looks like an alpha2 key. * * @throws \Automattic\WooCommerce\Vendor\League\ISO3166\Exception\DomainException if input does not look like an alpha2 key */ public static function guardAgainstInvalidAlpha2(string $alpha2): void { if (1 !== preg_match('/^[a-zA-Z]{2}$/', $alpha2)) { throw new DomainException(sprintf('Not a valid alpha2 key: %s', $alpha2)); } } /** * Assert that input looks like an alpha3 key. * * @throws \Automattic\WooCommerce\Vendor\League\ISO3166\Exception\DomainException if input does not look like an alpha3 key */ public static function guardAgainstInvalidAlpha3(string $alpha3): void { if (1 !== preg_match('/^[a-zA-Z]{3}$/', $alpha3)) { throw new DomainException(sprintf('Not a valid alpha3 key: %s', $alpha3)); } } /** * Assert that input looks like a numeric key. * * @throws \Automattic\WooCommerce\Vendor\League\ISO3166\Exception\DomainException if input does not look like a numeric key */ public static function guardAgainstInvalidNumeric(string $numeric): void { if (1 !== preg_match('/^\d{3}$/', $numeric)) { throw new DomainException(sprintf('Not a valid numeric key: %s', $numeric)); } } } packages/League/ISO3166/Exception/DomainException.php 0000644 00000000550 15217646633 0016250 0 ustar 00 <?php declare(strict_types=1); /* * (c) Rob Bast <rob.bast@gmail.com> * * For the full copyright and license information, please view * the LICENSE file that was distributed with this source code. */ namespace Automattic\WooCommerce\Vendor\League\ISO3166\Exception; final class DomainException extends \DomainException implements ISO3166Exception { } packages/League/ISO3166/Exception/ISO3166Exception.php 0000644 00000000505 15217646633 0016013 0 ustar 00 <?php declare(strict_types=1); /* * (c) Rob Bast <rob.bast@gmail.com> * * For the full copyright and license information, please view * the LICENSE file that was distributed with this source code. */ namespace Automattic\WooCommerce\Vendor\League\ISO3166\Exception; interface ISO3166Exception extends \Throwable { } packages/League/ISO3166/Exception/OutOfBoundsException.php 0000644 00000000562 15217646633 0017253 0 ustar 00 <?php declare(strict_types=1); /* * (c) Rob Bast <rob.bast@gmail.com> * * For the full copyright and license information, please view * the LICENSE file that was distributed with this source code. */ namespace Automattic\WooCommerce\Vendor\League\ISO3166\Exception; final class OutOfBoundsException extends \OutOfBoundsException implements ISO3166Exception { } packages/League/ISO3166/ISO3166DataProvider.php 0000644 00000003662 15217646634 0014513 0 ustar 00 <?php declare(strict_types=1); /* * (c) Rob Bast <rob.bast@gmail.com> * * For the full copyright and license information, please view * the LICENSE file that was distributed with this source code. */ namespace Automattic\WooCommerce\Vendor\League\ISO3166; interface ISO3166DataProvider { /** * Lookup ISO3166-1 data by name identifier. * * @api * * @throws \Automattic\WooCommerce\Vendor\League\ISO3166\Exception\OutOfBoundsException if input does not exist in dataset * * @return array<string, mixed> */ public function name(string $name): array; /** * Lookup ISO3166-1 data by alpha2 identifier. * * @api * * @throws \Automattic\WooCommerce\Vendor\League\ISO3166\Exception\DomainException if input does not look like an alpha2 key * @throws \Automattic\WooCommerce\Vendor\League\ISO3166\Exception\OutOfBoundsException if input does not exist in dataset * * @return array<string, mixed> */ public function alpha2(string $alpha2): array; /** * Lookup ISO3166-1 data by alpha3 identifier. * * @api * * @throws \Automattic\WooCommerce\Vendor\League\ISO3166\Exception\DomainException if input does not look like an alpha3 key * @throws \Automattic\WooCommerce\Vendor\League\ISO3166\Exception\OutOfBoundsException if input does not exist in dataset * * @return array<string, mixed> */ public function alpha3(string $alpha3): array; /** * Lookup ISO3166-1 data by numeric identifier (numerical string, that is). * * @api * * @throws \Automattic\WooCommerce\Vendor\League\ISO3166\Exception\DomainException if input does not look like a numeric key * @throws \Automattic\WooCommerce\Vendor\League\ISO3166\Exception\OutOfBoundsException if input does not exist in dataset * * @return array<string, mixed> */ public function numeric(string $numeric): array; }
| ver. 1.4 |
Github
|
.
| PHP 8.1.34 | Generation time: 0.26 |
proxy
|
phpinfo
|
Settings