Файловый менеджер - Редактировать - /home/tuudkjt/globeasy/wp-includes/ID3/packages.tar
Назад
email-editor/composer.json 0000777 00000003345 15252010675 0011660 0 ustar 00 { "name": "woocommerce/email-editor", "description": "Email editor based on WordPress Gutenberg package.", "type": "wordpress-plugin", "license": "GPL-2.0-or-later", "prefer-stable": true, "minimum-stability": "dev", "version": "2.3.0", "autoload": { "classmap": [ "src/", "vendor-prefixed/" ] }, "autoload-dev": { "classmap": [ "tests/unit/" ] }, "require": { "php": ">=7.4" }, "require-dev": { "automattic/jetpack-changelogger": "3.3.0", "phpunit/phpunit": "^9.6", "woocommerce/woocommerce-sniffs": "1.0.0", "yoast/phpunit-polyfills": "^4.0" }, "config": { "platform": { "php": "7.4" }, "allow-plugins": { "dealerdirect/phpcodesniffer-composer-installer": true } }, "scripts": { "post-install-cmd": [ "composer dump-autoload -o" ], "post-update-cmd": [ "composer dump-autoload -o" ], "env:destroy": "wp-env destroy", "env:stop": "wp-env stop", "env:start": "wp-env start", "test:unit": "wp-env run tests-cli --env-cwd=wp-content/plugins/email-editor ./vendor/bin/phpunit", "test:integration": "wp-env run tests-cli --env-cwd=wp-content/plugins/email-editor ./vendor/bin/phpunit --configuration phpunit-integration.xml.dist", "phpcs": "phpcs -s -p", "phpcbf": "phpcbf -p" }, "extra": { "changelogger": { "formatter": { "filename": "../../../tools/changelogger/class-php-package-formatter.php" }, "types": { "fix": "Fixes an existing bug", "add": "Adds functionality", "update": "Update existing functionality", "dev": "Development related task", "tweak": "A minor adjustment to the codebase", "performance": "Address performance issues", "enhancement": "Improve existing functionality" }, "changelog": "changelog.md" } } } email-editor/vendor-prefixed/LICENSE 0000777 00000001276 15252010675 0013245 0 ustar 00 The folder contains auto generated content. This program incorporates work covered by the following copyright and permission notices: Pelago/Emogrifier pelago/emogrifier is Copyright (c) 2008-2018 Pelago https://github.com/MyIntervals/emogrifier pelago/emogrifier is released under the MIT License https://github.com/MyIntervals/emogrifier/blob/2cb1701ab3f23e979a7201d20615b33e31c365d3/LICENSE Sabberworm/PHP CSS Parser sabberworm/php-css-parser is Copyright (c) 2011 Raphael Schweikert, https://www.sabberworm.com/ sabberworm/php-css-parser is released under the MIT License https://github.com/MyIntervals/PHP-CSS-Parser/blob/92432abc08758613a166fc5af5fa89c15aca4af3/LICENSE email-editor/vendor-prefixed/setup.sh 0000777 00000001072 15252010675 0013726 0 ustar 00 #!/usr/bin/env bash set -e # Exit immediately on error cd "$(dirname "$0")" # Ensure we're in the correct directory # Ensure composer is installed if ! command -v composer >/dev/null 2>&1; then echo "❌ Composer is not installed. Please install Composer first." exit 1 fi # Skip if we are not in the monorepo if [ ! -f "../../../../plugins/woocommerce/vendor/bin/mozart" ]; then echo "❌ We are not in the monorepo. Skipping vendor-prefixed setup." exit 0 # Returning 0 incase we are in CI environment fi # Run composer install composer install --quiet email-editor/vendor-prefixed/classes/symfony/polyfill-php80/Resources/stubs/PhpToken.php 0000777 00000000633 15252010675 0025537 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 EmailEditorVendor_PhpToken extends Symfony\Polyfill\Php80\EmailEditorVendor_PhpToken { } } email-editor/vendor-prefixed/classes/symfony/polyfill-php80/Resources/stubs/Attribute.php 0000777 00000001424 15252010675 0025751 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. */ #[EmailEditorVendor_Attribute(Attribute::TARGET_CLASS)] final class EmailEditorVendor_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; } } email-editor/vendor-prefixed/classes/symfony/polyfill-php80/Resources/stubs/ValueError.php 0000777 00000000520 15252010675 0026070 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 EmailEditorVendor_ValueError extends Error { } } email-editor/vendor-prefixed/classes/symfony/polyfill-php80/Resources/stubs/Stringable.php 0000777 00000000636 15252010675 0026104 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 EmailEditorVendor_Stringable { /** * @return string */ public function __toString(); } } email-editor/vendor-prefixed/classes/symfony/polyfill-php80/Resources/stubs/UnhandledMatchError.php 0000777 00000000531 15252010675 0027675 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 EmailEditorVendor_UnhandledMatchError extends Error { } } email-editor/vendor-prefixed/packages/Symfony/Polyfill/Php80/LICENSE 0000777 00000002054 15252010675 0021153 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. email-editor/vendor-prefixed/packages/Symfony/Polyfill/Php80/composer.json 0000777 00000001765 15252010675 0022700 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" } } } email-editor/vendor-prefixed/packages/Symfony/Polyfill/Php80/bootstrap.php 0000777 00000003045 15252010675 0022675 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\EmailEditorVendor\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); } } email-editor/vendor-prefixed/packages/Symfony/Polyfill/Php80/PhpToken.php 0000777 00000004366 15252010675 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\EmailEditorVendor\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; } } email-editor/vendor-prefixed/packages/Symfony/Polyfill/Php80/Php80.php 0000777 00000007042 15252010675 0021560 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\EmailEditorVendor\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); } } email-editor/vendor-prefixed/packages/Symfony/Polyfill/Php80/Resources/stubs/PhpToken.php 0000777 00000000640 15252010675 0025520 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\EmailEditorVendor\Symfony\Polyfill\Php80\PhpToken { } } email-editor/vendor-prefixed/packages/Symfony/Polyfill/Php80/Resources/stubs/ValueError.php 0000777 00000000476 15252010675 0026065 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 { } } email-editor/vendor-prefixed/packages/Symfony/Polyfill/Php80/Resources/stubs/UnhandledMatchError.php0000777 00000000507 15252010675 0027663 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 { } } email-editor/vendor-prefixed/packages/Symfony/Polyfill/Php80/Resources/stubs/Stringable.php 0000777 00000000614 15252010675 0026063 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(); } } email-editor/vendor-prefixed/packages/Symfony/Polyfill/Php80/Resources/stubs/Attribute.php 0000777 00000001360 15252010675 0025733 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; } } email-editor/vendor-prefixed/packages/Symfony/Component/CssSelector/Parser/TokenStream.php 0000777 00000006663 15252010675 0026063 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\EmailEditorVendor\Symfony\Component\CssSelector\Parser; use Automattic\WooCommerce\EmailEditorVendor\Symfony\Component\CssSelector\Exception\InternalErrorException; use Automattic\WooCommerce\EmailEditorVendor\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(); } } } email-editor/vendor-prefixed/packages/Symfony/Component/CssSelector/Parser/Parser.php 0000777 00000027007 15252010675 0025056 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\EmailEditorVendor\Symfony\Component\CssSelector\Parser; use Automattic\WooCommerce\EmailEditorVendor\Symfony\Component\CssSelector\Exception\SyntaxErrorException; use Automattic\WooCommerce\EmailEditorVendor\Symfony\Component\CssSelector\Node; use Automattic\WooCommerce\EmailEditorVendor\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()); } } vendor-prefixed/packages/Symfony/Component/CssSelector/Parser/Tokenizer/TokenizerPatterns.php 0000777 00000005403 15252010675 0031204 0 ustar 00 email-editor <?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\EmailEditorVendor\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'; } } vendor-prefixed/packages/Symfony/Component/CssSelector/Parser/Tokenizer/TokenizerEscaping.php 0000777 00000003431 15252010675 0031134 0 ustar 00 email-editor <?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\EmailEditorVendor\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); } } email-editor/vendor-prefixed/packages/Symfony/Component/CssSelector/Parser/Tokenizer/Tokenizer.php 0000777 00000004277 15252010675 0027552 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\EmailEditorVendor\Symfony\Component\CssSelector\Parser\Tokenizer; use Automattic\WooCommerce\EmailEditorVendor\Symfony\Component\CssSelector\Parser\Handler; use Automattic\WooCommerce\EmailEditorVendor\Symfony\Component\CssSelector\Parser\Reader; use Automattic\WooCommerce\EmailEditorVendor\Symfony\Component\CssSelector\Parser\Token; use Automattic\WooCommerce\EmailEditorVendor\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(); } } email-editor/vendor-prefixed/packages/Symfony/Component/CssSelector/Parser/ParserInterface.php 0000777 00000001573 15252010675 0026677 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\EmailEditorVendor\Symfony\Component\CssSelector\Parser; use Automattic\WooCommerce\EmailEditorVendor\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; } email-editor/vendor-prefixed/packages/Symfony/Component/CssSelector/Parser/Handler/HashHandler.php 0000777 00000003472 15252010675 0027360 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\EmailEditorVendor\Symfony\Component\CssSelector\Parser\Handler; use Automattic\WooCommerce\EmailEditorVendor\Symfony\Component\CssSelector\Parser\Reader; use Automattic\WooCommerce\EmailEditorVendor\Symfony\Component\CssSelector\Parser\Token; use Automattic\WooCommerce\EmailEditorVendor\Symfony\Component\CssSelector\Parser\Tokenizer\TokenizerEscaping; use Automattic\WooCommerce\EmailEditorVendor\Symfony\Component\CssSelector\Parser\Tokenizer\TokenizerPatterns; use Automattic\WooCommerce\EmailEditorVendor\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; } } vendor-prefixed/packages/Symfony/Component/CssSelector/Parser/Handler/CommentHandler.php 0000777 00000002355 15252010675 0030017 0 ustar 00 email-editor <?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\EmailEditorVendor\Symfony\Component\CssSelector\Parser\Handler; use Automattic\WooCommerce\EmailEditorVendor\Symfony\Component\CssSelector\Parser\Reader; use Automattic\WooCommerce\EmailEditorVendor\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; } } vendor-prefixed/packages/Symfony/Component/CssSelector/Parser/Handler/IdentifierHandler.php 0000777 00000003514 15252010675 0030475 0 ustar 00 email-editor <?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\EmailEditorVendor\Symfony\Component\CssSelector\Parser\Handler; use Automattic\WooCommerce\EmailEditorVendor\Symfony\Component\CssSelector\Parser\Reader; use Automattic\WooCommerce\EmailEditorVendor\Symfony\Component\CssSelector\Parser\Token; use Automattic\WooCommerce\EmailEditorVendor\Symfony\Component\CssSelector\Parser\Tokenizer\TokenizerEscaping; use Automattic\WooCommerce\EmailEditorVendor\Symfony\Component\CssSelector\Parser\Tokenizer\TokenizerPatterns; use Automattic\WooCommerce\EmailEditorVendor\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; } } email-editor/vendor-prefixed/packages/Symfony/Component/CssSelector/Parser/Handler/NumberHandler.php0000777 00000003077 15252010675 0027726 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\EmailEditorVendor\Symfony\Component\CssSelector\Parser\Handler; use Automattic\WooCommerce\EmailEditorVendor\Symfony\Component\CssSelector\Parser\Reader; use Automattic\WooCommerce\EmailEditorVendor\Symfony\Component\CssSelector\Parser\Token; use Automattic\WooCommerce\EmailEditorVendor\Symfony\Component\CssSelector\Parser\Tokenizer\TokenizerPatterns; use Automattic\WooCommerce\EmailEditorVendor\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; } } vendor-prefixed/packages/Symfony/Component/CssSelector/Parser/Handler/WhitespaceHandler.php 0000777 00000002513 15252010675 0030505 0 ustar 00 email-editor <?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\EmailEditorVendor\Symfony\Component\CssSelector\Parser\Handler; use Automattic\WooCommerce\EmailEditorVendor\Symfony\Component\CssSelector\Parser\Reader; use Automattic\WooCommerce\EmailEditorVendor\Symfony\Component\CssSelector\Parser\Token; use Automattic\WooCommerce\EmailEditorVendor\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; } } email-editor/vendor-prefixed/packages/Symfony/Component/CssSelector/Parser/Handler/StringHandler.php0000777 00000005317 15252010675 0027743 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\EmailEditorVendor\Symfony\Component\CssSelector\Parser\Handler; use Automattic\WooCommerce\EmailEditorVendor\Symfony\Component\CssSelector\Exception\InternalErrorException; use Automattic\WooCommerce\EmailEditorVendor\Symfony\Component\CssSelector\Exception\SyntaxErrorException; use Automattic\WooCommerce\EmailEditorVendor\Symfony\Component\CssSelector\Parser\Reader; use Automattic\WooCommerce\EmailEditorVendor\Symfony\Component\CssSelector\Parser\Token; use Automattic\WooCommerce\EmailEditorVendor\Symfony\Component\CssSelector\Parser\Tokenizer\TokenizerEscaping; use Automattic\WooCommerce\EmailEditorVendor\Symfony\Component\CssSelector\Parser\Tokenizer\TokenizerPatterns; use Automattic\WooCommerce\EmailEditorVendor\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; } } vendor-prefixed/packages/Symfony/Component/CssSelector/Parser/Handler/HandlerInterface.php 0000777 00000001603 15252010675 0030310 0 ustar 00 email-editor <?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\EmailEditorVendor\Symfony\Component\CssSelector\Parser\Handler; use Automattic\WooCommerce\EmailEditorVendor\Symfony\Component\CssSelector\Parser\Reader; use Automattic\WooCommerce\EmailEditorVendor\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; } email-editor/vendor-prefixed/packages/Symfony/Component/CssSelector/Parser/Reader.php 0000777 00000003603 15252010675 0025020 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\EmailEditorVendor\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; } } email-editor/vendor-prefixed/packages/Symfony/Component/CssSelector/Parser/Shortcut/ClassParser.php 0000777 00000003411 15252010675 0027650 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\EmailEditorVendor\Symfony\Component\CssSelector\Parser\Shortcut; use Automattic\WooCommerce\EmailEditorVendor\Symfony\Component\CssSelector\Node\ClassNode; use Automattic\WooCommerce\EmailEditorVendor\Symfony\Component\CssSelector\Node\ElementNode; use Automattic\WooCommerce\EmailEditorVendor\Symfony\Component\CssSelector\Node\SelectorNode; use Automattic\WooCommerce\EmailEditorVendor\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 []; } } vendor-prefixed/packages/Symfony/Component/CssSelector/Parser/Shortcut/ElementParser.php 0000777 00000003020 15252010675 0030111 0 ustar 00 email-editor <?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\EmailEditorVendor\Symfony\Component\CssSelector\Parser\Shortcut; use Automattic\WooCommerce\EmailEditorVendor\Symfony\Component\CssSelector\Node\ElementNode; use Automattic\WooCommerce\EmailEditorVendor\Symfony\Component\CssSelector\Node\SelectorNode; use Automattic\WooCommerce\EmailEditorVendor\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 []; } } vendor-prefixed/packages/Symfony/Component/CssSelector/Parser/Shortcut/EmptyStringParser.php 0000777 00000002562 15252010675 0031017 0 ustar 00 email-editor <?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\EmailEditorVendor\Symfony\Component\CssSelector\Parser\Shortcut; use Automattic\WooCommerce\EmailEditorVendor\Symfony\Component\CssSelector\Node\ElementNode; use Automattic\WooCommerce\EmailEditorVendor\Symfony\Component\CssSelector\Node\SelectorNode; use Automattic\WooCommerce\EmailEditorVendor\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 []; } } email-editor/vendor-prefixed/packages/Symfony/Component/CssSelector/Parser/Shortcut/HashParser.php 0000777 00000003401 15252010675 0027465 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\EmailEditorVendor\Symfony\Component\CssSelector\Parser\Shortcut; use Automattic\WooCommerce\EmailEditorVendor\Symfony\Component\CssSelector\Node\ElementNode; use Automattic\WooCommerce\EmailEditorVendor\Symfony\Component\CssSelector\Node\HashNode; use Automattic\WooCommerce\EmailEditorVendor\Symfony\Component\CssSelector\Node\SelectorNode; use Automattic\WooCommerce\EmailEditorVendor\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 []; } } email-editor/vendor-prefixed/packages/Symfony/Component/CssSelector/Parser/Token.php 0000777 00000004765 15252010675 0024710 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\EmailEditorVendor\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); } } email-editor/vendor-prefixed/packages/Symfony/Component/CssSelector/XPath/XPathExpr.php 0000777 00000004741 15252010675 0025275 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\EmailEditorVendor\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/AttributeMatchingExtension.php 0000777 00000007551 15252010675 0032624 0 ustar 00 email-editor/vendor-prefixed <?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\EmailEditorVendor\Symfony\Component\CssSelector\XPath\Extension; use Automattic\WooCommerce\EmailEditorVendor\Symfony\Component\CssSelector\XPath\Translator; use Automattic\WooCommerce\EmailEditorVendor\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'; } } vendor-prefixed/packages/Symfony/Component/CssSelector/XPath/Extension/CombinationExtension.php 0000777 00000003747 15252010675 0031453 0 ustar 00 email-editor <?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\EmailEditorVendor\Symfony\Component\CssSelector\XPath\Extension; use Automattic\WooCommerce\EmailEditorVendor\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'; } } vendor-prefixed/packages/Symfony/Component/CssSelector/XPath/Extension/PseudoClassExtension.php 0000777 00000006720 15252010675 0031430 0 ustar 00 email-editor <?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\EmailEditorVendor\Symfony\Component\CssSelector\XPath\Extension; use Automattic\WooCommerce\EmailEditorVendor\Symfony\Component\CssSelector\Exception\ExpressionErrorException; use Automattic\WooCommerce\EmailEditorVendor\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'; } } vendor-prefixed/packages/Symfony/Component/CssSelector/XPath/Extension/ExtensionInterface.php 0000777 00000003056 15252010675 0031102 0 ustar 00 email-editor <?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\EmailEditorVendor\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; } vendor-prefixed/packages/Symfony/Component/CssSelector/XPath/Extension/HtmlExtension.php 0000777 00000013743 15252010675 0030112 0 ustar 00 email-editor <?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\EmailEditorVendor\Symfony\Component\CssSelector\XPath\Extension; use Automattic\WooCommerce\EmailEditorVendor\Symfony\Component\CssSelector\Exception\ExpressionErrorException; use Automattic\WooCommerce\EmailEditorVendor\Symfony\Component\CssSelector\Node\FunctionNode; use Automattic\WooCommerce\EmailEditorVendor\Symfony\Component\CssSelector\XPath\Translator; use Automattic\WooCommerce\EmailEditorVendor\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'; } } vendor-prefixed/packages/Symfony/Component/CssSelector/XPath/Extension/NodeExtension.php 0000777 00000013661 15252010675 0030072 0 ustar 00 email-editor <?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\EmailEditorVendor\Symfony\Component\CssSelector\XPath\Extension; use Automattic\WooCommerce\EmailEditorVendor\Symfony\Component\CssSelector\Node; use Automattic\WooCommerce\EmailEditorVendor\Symfony\Component\CssSelector\XPath\Translator; use Automattic\WooCommerce\EmailEditorVendor\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'], 'EmailEditorVendor_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); } } vendor-prefixed/packages/Symfony/Component/CssSelector/XPath/Extension/FunctionExtension.php 0000777 00000012613 15252010675 0030766 0 ustar 00 email-editor <?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\EmailEditorVendor\Symfony\Component\CssSelector\XPath\Extension; use Automattic\WooCommerce\EmailEditorVendor\Symfony\Component\CssSelector\Exception\ExpressionErrorException; use Automattic\WooCommerce\EmailEditorVendor\Symfony\Component\CssSelector\Exception\SyntaxErrorException; use Automattic\WooCommerce\EmailEditorVendor\Symfony\Component\CssSelector\Node\FunctionNode; use Automattic\WooCommerce\EmailEditorVendor\Symfony\Component\CssSelector\Parser\Parser; use Automattic\WooCommerce\EmailEditorVendor\Symfony\Component\CssSelector\XPath\Translator; use Automattic\WooCommerce\EmailEditorVendor\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'; } } vendor-prefixed/packages/Symfony/Component/CssSelector/XPath/Extension/AbstractExtension.php 0000777 00000002436 15252010675 0030746 0 ustar 00 email-editor <?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\EmailEditorVendor\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 []; } } email-editor/vendor-prefixed/packages/Symfony/Component/CssSelector/XPath/Translator.php 0000777 00000016721 15252010675 0025544 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\EmailEditorVendor\Symfony\Component\CssSelector\XPath; use Automattic\WooCommerce\EmailEditorVendor\Symfony\Component\CssSelector\Exception\ExpressionErrorException; use Automattic\WooCommerce\EmailEditorVendor\Symfony\Component\CssSelector\Node\FunctionNode; use Automattic\WooCommerce\EmailEditorVendor\Symfony\Component\CssSelector\Node\NodeInterface; use Automattic\WooCommerce\EmailEditorVendor\Symfony\Component\CssSelector\Node\SelectorNode; use Automattic\WooCommerce\EmailEditorVendor\Symfony\Component\CssSelector\Parser\Parser; use Automattic\WooCommerce\EmailEditorVendor\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('EmailEditorVendor_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); } } email-editor/vendor-prefixed/packages/Symfony/Component/CssSelector/XPath/TranslatorInterface.php 0000777 00000002115 15252010675 0027355 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\EmailEditorVendor\Symfony\Component\CssSelector\XPath; use Automattic\WooCommerce\EmailEditorVendor\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; } email-editor/vendor-prefixed/packages/Symfony/Component/CssSelector/Node/HashNode.php 0000777 00000002452 15252010675 0024741 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\EmailEditorVendor\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); } } email-editor/vendor-prefixed/packages/Symfony/Component/CssSelector/Node/SelectorNode.php 0000777 00000002723 15252010675 0025637 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\EmailEditorVendor\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 : ''); } } email-editor/vendor-prefixed/packages/Symfony/Component/CssSelector/Node/ElementNode.php 0000777 00000002620 15252010675 0025444 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\EmailEditorVendor\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); } } email-editor/vendor-prefixed/packages/Symfony/Component/CssSelector/Node/AbstractNode.php 0000777 00000001654 15252010675 0025624 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\EmailEditorVendor\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; } } email-editor/vendor-prefixed/packages/Symfony/Component/CssSelector/Node/PseudoNode.php 0000777 00000002570 15252010675 0025316 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\EmailEditorVendor\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); } } email-editor/vendor-prefixed/packages/Symfony/Component/CssSelector/Node/Specificity.php 0000777 00000003465 15252010675 0025530 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\EmailEditorVendor\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; } } email-editor/vendor-prefixed/packages/Symfony/Component/CssSelector/Node/NodeInterface.php 0000777 00000001364 15252010675 0025757 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\EmailEditorVendor\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; } email-editor/vendor-prefixed/packages/Symfony/Component/CssSelector/Node/NegationNode.php 0000777 00000002631 15252010675 0025621 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\EmailEditorVendor\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); } } email-editor/vendor-prefixed/packages/Symfony/Component/CssSelector/Node/AttributeNode.php 0000777 00000004165 15252010675 0026024 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\EmailEditorVendor\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); } } email-editor/vendor-prefixed/packages/Symfony/Component/CssSelector/Node/FunctionNode.php 0000777 00000003567 15252010675 0025653 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\EmailEditorVendor\Symfony\Component\CssSelector\Node; use Automattic\WooCommerce\EmailEditorVendor\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.']' : ''); } } email-editor/vendor-prefixed/packages/Symfony/Component/CssSelector/Node/CombinedSelectorNode.php 0000777 00000003234 15252010675 0027276 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\EmailEditorVendor\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); } } email-editor/vendor-prefixed/packages/Symfony/Component/CssSelector/Node/ClassNode.php 0000777 00000002473 15252010675 0025126 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\EmailEditorVendor\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); } } email-editor/vendor-prefixed/packages/Symfony/Component/CssSelector/CssSelectorConverter.php 0000777 00000004610 15252010675 0026502 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\EmailEditorVendor\Symfony\Component\CssSelector; use Automattic\WooCommerce\EmailEditorVendor\Symfony\Component\CssSelector\Parser\Shortcut\ClassParser; use Automattic\WooCommerce\EmailEditorVendor\Symfony\Component\CssSelector\Parser\Shortcut\ElementParser; use Automattic\WooCommerce\EmailEditorVendor\Symfony\Component\CssSelector\Parser\Shortcut\EmptyStringParser; use Automattic\WooCommerce\EmailEditorVendor\Symfony\Component\CssSelector\Parser\Shortcut\HashParser; use Automattic\WooCommerce\EmailEditorVendor\Symfony\Component\CssSelector\XPath\Extension\HtmlExtension; use Automattic\WooCommerce\EmailEditorVendor\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); } } email-editor/vendor-prefixed/packages/Symfony/Component/CssSelector/composer.json 0000777 00000001530 15252010675 0024370 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" } vendor-prefixed/packages/Symfony/Component/CssSelector/Exception/SyntaxErrorException.php 0000777 00000003326 15252010675 0030422 0 ustar 00 email-editor <?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\EmailEditorVendor\Symfony\Component\CssSelector\Exception; use Automattic\WooCommerce\EmailEditorVendor\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.'); } } email-editor/vendor-prefixed/packages/Symfony/Component/CssSelector/Exception/ParseException.php 0000777 00000001247 15252010675 0027253 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\EmailEditorVendor\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 { } email-editor/vendor-prefixed/packages/Symfony/Component/CssSelector/Exception/ExceptionInterface.php0000777 00000001174 15252010675 0030100 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\EmailEditorVendor\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 { } vendor-prefixed/packages/Symfony/Component/CssSelector/Exception/ExpressionErrorException.php 0000777 00000001252 15252010675 0031267 0 ustar 00 email-editor <?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\EmailEditorVendor\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 { } vendor-prefixed/packages/Symfony/Component/CssSelector/Exception/InternalErrorException.php 0000777 00000001250 15252010675 0030702 0 ustar 00 email-editor <?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\EmailEditorVendor\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 { } email-editor/vendor-prefixed/packages/Symfony/Component/CssSelector/LICENSE 0000777 00000002054 15252010675 0022655 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. email-editor/vendor-prefixed/packages/Pelago/Emogrifier/Css/CssDocument.php 0000777 00000017566 15252010675 0023016 0 ustar 00 <?php declare(strict_types=1); namespace Automattic\WooCommerce\EmailEditorVendor\Pelago\Emogrifier\Css; use Automattic\WooCommerce\EmailEditorVendor\Pelago\Emogrifier\Utilities\Preg; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\CSSList\AtRuleBlockList as CssAtRuleBlockList; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\CSSList\Document as SabberwormCssDocument; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Parser as CssParser; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Property\AtRule as CssAtRule; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Property\Charset as CssCharset; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Property\Import as CssImport; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Renderable as CssRenderable; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\RuleSet\DeclarationBlock as CssDeclarationBlock; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\RuleSet\RuleSet as CssRuleSet; use Automattic\WooCommerce\EmailEditorVendor\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; } } email-editor/vendor-prefixed/packages/Pelago/Emogrifier/Css/StyleRule.php 0000777 00000004376 15252010675 0022512 0 ustar 00 <?php declare(strict_types=1); namespace Automattic\WooCommerce\EmailEditorVendor\Pelago\Emogrifier\Css; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Property\Selector; use Automattic\WooCommerce\EmailEditorVendor\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() !== ''; } } email-editor/vendor-prefixed/packages/Pelago/Emogrifier/Caching/SimpleStringCache.php 0000777 00000004110 15252010675 0024714 0 ustar 00 <?php declare(strict_types=1); namespace Automattic\WooCommerce\EmailEditorVendor\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); } } } email-editor/vendor-prefixed/packages/Pelago/Emogrifier/CssInliner.php 0000777 00000127065 15252010675 0022104 0 ustar 00 <?php declare(strict_types=1); namespace Automattic\WooCommerce\EmailEditorVendor\Pelago\Emogrifier; use Automattic\WooCommerce\EmailEditorVendor\Pelago\Emogrifier\Css\CssDocument; use Automattic\WooCommerce\EmailEditorVendor\Pelago\Emogrifier\HtmlProcessor\AbstractHtmlProcessor; use Automattic\WooCommerce\EmailEditorVendor\Pelago\Emogrifier\Utilities\CssConcatenator; use Automattic\WooCommerce\EmailEditorVendor\Pelago\Emogrifier\Utilities\DeclarationBlockParser; use Automattic\WooCommerce\EmailEditorVendor\Pelago\Emogrifier\Utilities\Preg; use Automattic\WooCommerce\EmailEditorVendor\Symfony\Component\CssSelector\CssSelectorConverter; use Automattic\WooCommerce\EmailEditorVendor\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; } } email-editor/vendor-prefixed/packages/Pelago/Emogrifier/Utilities/ArrayIntersector.php 0000777 00000003637 15252010675 0025304 0 ustar 00 <?php declare(strict_types=1); namespace Automattic\WooCommerce\EmailEditorVendor\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); } } email-editor/vendor-prefixed/packages/Pelago/Emogrifier/Utilities/CssConcatenator.php 0000777 00000015177 15252010675 0025077 0 ustar 00 <?php declare(strict_types=1); namespace Automattic\WooCommerce\EmailEditorVendor\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 . '}'; } } email-editor/vendor-prefixed/packages/Pelago/Emogrifier/Utilities/Preg.php 0000777 00000015570 15252010675 0022700 0 ustar 00 <?php declare(strict_types=1); namespace Automattic\WooCommerce\EmailEditorVendor\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); } } email-editor/vendor-prefixed/packages/Pelago/Emogrifier/Utilities/DeclarationBlockParser.php 0000777 00000005502 15252010675 0026352 0 ustar 00 <?php declare(strict_types=1); namespace Automattic\WooCommerce\EmailEditorVendor\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; } } email-editor/vendor-prefixed/packages/Pelago/Emogrifier/HtmlProcessor/AbstractHtmlProcessor.php 0000777 00000035737 15252010675 0027133 0 ustar 00 <?php declare(strict_types=1); namespace Automattic\WooCommerce\EmailEditorVendor\Pelago\Emogrifier\HtmlProcessor; use Automattic\WooCommerce\EmailEditorVendor\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')); } } email-editor/vendor-prefixed/packages/Pelago/Emogrifier/HtmlProcessor/HtmlPruner.php 0000777 00000012342 15252010675 0024726 0 ustar 00 <?php declare(strict_types=1); namespace Automattic\WooCommerce\EmailEditorVendor\Pelago\Emogrifier\HtmlProcessor; use Automattic\WooCommerce\EmailEditorVendor\Pelago\Emogrifier\CssInliner; use Automattic\WooCommerce\EmailEditorVendor\Pelago\Emogrifier\Utilities\ArrayIntersector; use Automattic\WooCommerce\EmailEditorVendor\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; } } email-editor/vendor-prefixed/packages/Pelago/Emogrifier/HtmlProcessor/CssToAttributeConverter.php 0000777 00000021512 15252010675 0027434 0 ustar 00 <?php declare(strict_types=1); namespace Automattic\WooCommerce\EmailEditorVendor\Pelago\Emogrifier\HtmlProcessor; use Automattic\WooCommerce\EmailEditorVendor\Pelago\Emogrifier\Utilities\DeclarationBlockParser; use Automattic\WooCommerce\EmailEditorVendor\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; } } email-editor/vendor-prefixed/packages/Pelago/Emogrifier/HtmlProcessor/HtmlNormalizer.php 0000777 00000000560 15252010675 0025574 0 ustar 00 <?php declare(strict_types=1); namespace Automattic\WooCommerce\EmailEditorVendor\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 {} email-editor/vendor-prefixed/packages/Pelago/Emogrifier/HtmlProcessor/CssVariableEvaluator.php 0000777 00000016324 15252010675 0026713 0 ustar 00 <?php declare(strict_types=1); namespace Automattic\WooCommerce\EmailEditorVendor\Pelago\Emogrifier\HtmlProcessor; use Automattic\WooCommerce\EmailEditorVendor\Pelago\Emogrifier\Utilities\DeclarationBlockParser; use Automattic\WooCommerce\EmailEditorVendor\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; } } email-editor/vendor-prefixed/packages/Sabberworm/CSS/Parser.php 0000777 00000003724 15252010675 0020516 0 ustar 00 <?php namespace Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\CSSList\Document; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Parsing\ParserState; use Automattic\WooCommerce\EmailEditorVendor\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); } } email-editor/vendor-prefixed/packages/Sabberworm/CSS/Comment/Comment.php 0000777 00000002711 15252010675 0022261 0 ustar 00 <?php namespace Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Comment; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\OutputFormat; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Renderable; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Position\Position; use Automattic\WooCommerce\EmailEditorVendor\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 . '*/'; } } email-editor/vendor-prefixed/packages/Sabberworm/CSS/Comment/Commentable.php 0000777 00000000755 15252010675 0023113 0 ustar 00 <?php namespace Automattic\WooCommerce\EmailEditorVendor\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); } email-editor/vendor-prefixed/packages/Sabberworm/CSS/CSSList/CSSBlockList.php 0000777 00000017156 15252010675 0023011 0 ustar 00 <?php namespace Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\CSSList; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\CSSElement; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Property\Selector; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Rule\Rule; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\RuleSet\DeclarationBlock; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\RuleSet\RuleSet; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Value\CSSFunction; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Value\Value; use Automattic\WooCommerce\EmailEditorVendor\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; } } } } } } email-editor/vendor-prefixed/packages/Sabberworm/CSS/CSSList/Document.php 0000777 00000010571 15252010675 0022322 0 ustar 00 <?php namespace Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\CSSList; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\OutputFormat; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Parsing\ParserState; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Parsing\SourceException; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Property\Selector; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\RuleSet\DeclarationBlock; use Automattic\WooCommerce\EmailEditorVendor\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; } } email-editor/vendor-prefixed/packages/Sabberworm/CSS/CSSList/AtRuleBlockList.php 0000777 00000003664 15252010675 0023554 0 ustar 00 <?php namespace Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\CSSList; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\OutputFormat; use Automattic\WooCommerce\EmailEditorVendor\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; } } email-editor/vendor-prefixed/packages/Sabberworm/CSS/CSSList/CSSList.php 0000777 00000041563 15252010675 0022035 0 ustar 00 <?php namespace Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\CSSList; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Comment\Comment; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Comment\Commentable; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\CSSElement; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\OutputFormat; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Parsing\ParserState; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Parsing\SourceException; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Parsing\UnexpectedEOFException; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Parsing\UnexpectedTokenException; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Position\Position; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Position\Positionable; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Property\AtRule; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Property\Charset; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Property\CSSNamespace; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Property\Import; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Property\Selector; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Renderable; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\RuleSet\AtRuleSet; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\RuleSet\DeclarationBlock; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\RuleSet\RuleSet; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Settings; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Value\CSSString; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Value\URL; use Automattic\WooCommerce\EmailEditorVendor\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; } } email-editor/vendor-prefixed/packages/Sabberworm/CSS/CSSList/KeyFrame.php 0000777 00000004310 15252010675 0022241 0 ustar 00 <?php namespace Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\CSSList; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\OutputFormat; use Automattic\WooCommerce\EmailEditorVendor\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; } } email-editor/vendor-prefixed/packages/Sabberworm/CSS/RuleSet/RuleSet.php 0000777 00000030514 15252010675 0022225 0 ustar 00 <?php namespace Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\RuleSet; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Comment\Comment; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Comment\Commentable; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\CSSElement; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\OutputFormat; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Parsing\ParserState; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Parsing\UnexpectedEOFException; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Parsing\UnexpectedTokenException; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Position\Position; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Position\Positionable; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Renderable; use Automattic\WooCommerce\EmailEditorVendor\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; } } email-editor/vendor-prefixed/packages/Sabberworm/CSS/RuleSet/DeclarationBlock.php 0000777 00000076321 15252010675 0024050 0 ustar 00 <?php namespace Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\RuleSet; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\CSSList\CSSList; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\CSSList\KeyFrame; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\OutputFormat; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Parsing\OutputException; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Parsing\ParserState; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Parsing\UnexpectedEOFException; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Parsing\UnexpectedTokenException; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Property\KeyframeSelector; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Property\Selector; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Rule\Rule; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Value\Color; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Value\RuleValueList; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Value\Size; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Value\URL; use Automattic\WooCommerce\EmailEditorVendor\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; } } email-editor/vendor-prefixed/packages/Sabberworm/CSS/RuleSet/AtRuleSet.php 0000777 00000003437 15252010675 0022516 0 ustar 00 <?php namespace Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\RuleSet; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\OutputFormat; use Automattic\WooCommerce\EmailEditorVendor\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; } } email-editor/vendor-prefixed/packages/Sabberworm/CSS/Rule/Rule.php 0000777 00000025531 15252010675 0021100 0 ustar 00 <?php namespace Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Rule; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Comment\Comment; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Comment\Commentable; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\CSSElement; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\OutputFormat; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Parsing\ParserState; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Parsing\UnexpectedEOFException; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Parsing\UnexpectedTokenException; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Position\Position; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Position\Positionable; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Value\RuleValueList; use Automattic\WooCommerce\EmailEditorVendor\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; } } email-editor/vendor-prefixed/packages/Sabberworm/CSS/Settings.php 0000777 00000005172 15252010675 0021061 0 ustar 00 <?php namespace Automattic\WooCommerce\EmailEditorVendor\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); } } email-editor/vendor-prefixed/packages/Sabberworm/CSS/Value/Value.php 0000777 00000020330 15252010675 0021402 0 ustar 00 <?php namespace Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Value; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\CSSElement; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Parsing\ParserState; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Parsing\SourceException; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Parsing\UnexpectedEOFException; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Parsing\UnexpectedTokenException; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Position\Position; use Automattic\WooCommerce\EmailEditorVendor\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}"; } } email-editor/vendor-prefixed/packages/Sabberworm/CSS/Value/CSSString.php 0000777 00000006317 15252010675 0022156 0 ustar 00 <?php namespace Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Value; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\OutputFormat; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Parsing\ParserState; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Parsing\SourceException; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Parsing\UnexpectedEOFException; use Automattic\WooCommerce\EmailEditorVendor\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(); } } email-editor/vendor-prefixed/packages/Sabberworm/CSS/Value/CSSFunction.php 0000777 00000006120 15252010675 0022465 0 ustar 00 <?php namespace Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Value; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\OutputFormat; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Parsing\ParserState; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Parsing\SourceException; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Parsing\UnexpectedEOFException; use Automattic\WooCommerce\EmailEditorVendor\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})"; } } email-editor/vendor-prefixed/packages/Sabberworm/CSS/Value/LineName.php 0000777 00000004172 15252010675 0022024 0 ustar 00 <?php namespace Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Value; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\OutputFormat; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Parsing\ParserState; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Parsing\UnexpectedEOFException; use Automattic\WooCommerce\EmailEditorVendor\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()) . ']'; } } email-editor/vendor-prefixed/packages/Sabberworm/CSS/Value/Size.php 0000777 00000014501 15252010675 0021243 0 ustar 00 <?php namespace Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Value; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\OutputFormat; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Parsing\ParserState; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Parsing\UnexpectedEOFException; use Automattic\WooCommerce\EmailEditorVendor\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); } } email-editor/vendor-prefixed/packages/Sabberworm/CSS/Value/URL.php 0000777 00000005057 15252010675 0021001 0 ustar 00 <?php namespace Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Value; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\OutputFormat; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Parsing\ParserState; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Parsing\SourceException; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Parsing\UnexpectedEOFException; use Automattic\WooCommerce\EmailEditorVendor\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)})"; } } email-editor/vendor-prefixed/packages/Sabberworm/CSS/Value/Color.php 0000777 00000015130 15252010675 0021406 0 ustar 00 <?php namespace Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Value; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\OutputFormat; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Parsing\ParserState; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Parsing\UnexpectedEOFException; use Automattic\WooCommerce\EmailEditorVendor\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); } } email-editor/vendor-prefixed/packages/Sabberworm/CSS/Value/ValueList.php 0000777 00000005511 15252010675 0022242 0 ustar 00 <?php namespace Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Value; use Automattic\WooCommerce\EmailEditorVendor\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 ); } } email-editor/vendor-prefixed/packages/Sabberworm/CSS/Value/CalcRuleValueList.php 0000777 00000001064 15252010675 0023654 0 ustar 00 <?php namespace Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Value; use Automattic\WooCommerce\EmailEditorVendor\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); } } email-editor/vendor-prefixed/packages/Sabberworm/CSS/Value/RuleValueList.php 0000777 00000001125 15252010675 0023067 0 ustar 00 <?php namespace Automattic\WooCommerce\EmailEditorVendor\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); } } email-editor/vendor-prefixed/packages/Sabberworm/CSS/Value/CalcFunction.php 0000777 00000010723 15252010675 0022703 0 ustar 00 <?php namespace Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Value; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Parsing\ParserState; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Parsing\UnexpectedEOFException; use Automattic\WooCommerce\EmailEditorVendor\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()); } } email-editor/vendor-prefixed/packages/Sabberworm/CSS/Value/PrimitiveValue.php 0000777 00000000415 15252010675 0023275 0 ustar 00 <?php namespace Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Value; abstract class PrimitiveValue extends Value { /** * @param int $iLineNo */ public function __construct($iLineNo = 0) { parent::__construct($iLineNo); } } email-editor/vendor-prefixed/packages/Sabberworm/CSS/Renderable.php 0000777 00000000720 15252010675 0021316 0 ustar 00 <?php namespace Automattic\WooCommerce\EmailEditorVendor\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(); } email-editor/vendor-prefixed/packages/Sabberworm/CSS/Position/Positionable.php 0000777 00000002325 15252010675 0023512 0 ustar 00 <?php declare(strict_types=1); namespace Automattic\WooCommerce\EmailEditorVendor\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); } email-editor/vendor-prefixed/packages/Sabberworm/CSS/Position/Position.php 0000777 00000002676 15252010675 0022677 0 ustar 00 <?php declare(strict_types=1); namespace Automattic\WooCommerce\EmailEditorVendor\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; } } email-editor/vendor-prefixed/packages/Sabberworm/CSS/Parsing/SourceException.php 0000777 00000001206 15252010675 0023775 0 ustar 00 <?php namespace Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Parsing; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Position\Position; use Automattic\WooCommerce\EmailEditorVendor\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); } } email-editor/vendor-prefixed/packages/Sabberworm/CSS/Parsing/ParserState.php 0000777 00000034415 15252010675 0023123 0 ustar 00 <?php namespace Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Parsing; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Comment\Comment; use Automattic\WooCommerce\EmailEditorVendor\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\EmailEditorVendor\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); } } } email-editor/vendor-prefixed/packages/Sabberworm/CSS/Parsing/UnexpectedTokenException.php 0000777 00000002755 15252010675 0025654 0 ustar 00 <?php namespace Automattic\WooCommerce\EmailEditorVendor\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); } } email-editor/vendor-prefixed/packages/Sabberworm/CSS/Parsing/OutputException.php 0000777 00000000617 15252010675 0024042 0 ustar 00 <?php namespace Automattic\WooCommerce\EmailEditorVendor\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); } } email-editor/vendor-prefixed/packages/Sabberworm/CSS/Parsing/Anchor.php 0000777 00000001415 15252010675 0022072 0 ustar 00 <?php namespace Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Parsing; /** * @internal since 8.7.0 */ class Anchor { /** * @var int */ private $iPosition; /** * @var \Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Parsing\ParserState */ private $oParserState; /** * @param int $iPosition * @param \Automattic\WooCommerce\EmailEditorVendor\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); } } email-editor/vendor-prefixed/packages/Sabberworm/CSS/Parsing/UnexpectedEOFException.php 0000777 00000000472 15252010675 0025177 0 ustar 00 <?php namespace Automattic\WooCommerce\EmailEditorVendor\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 { } email-editor/vendor-prefixed/packages/Sabberworm/CSS/CSSElement.php 0000777 00000001063 15252010675 0021216 0 ustar 00 <?php declare(strict_types=1); namespace Automattic\WooCommerce\EmailEditorVendor\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 {} email-editor/vendor-prefixed/packages/Sabberworm/CSS/Property/KeyframeSelector.php 0000777 00000001406 15252010675 0024345 0 ustar 00 <?php namespace Automattic\WooCommerce\EmailEditorVendor\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'; } email-editor/vendor-prefixed/packages/Sabberworm/CSS/Property/CSSNamespace.php 0000777 00000006034 15252010675 0023350 0 ustar 00 <?php namespace Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Property; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Comment\Comment; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\OutputFormat; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Position\Position; use Automattic\WooCommerce\EmailEditorVendor\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; } } email-editor/vendor-prefixed/packages/Sabberworm/CSS/Property/Import.php 0000777 00000005761 15252010675 0022363 0 ustar 00 <?php namespace Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Property; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Comment\Comment; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\OutputFormat; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Position\Position; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Position\Positionable; use Automattic\WooCommerce\EmailEditorVendor\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; } } email-editor/vendor-prefixed/packages/Sabberworm/CSS/Property/Selector.php 0000777 00000007227 15252010675 0022670 0 ustar 00 <?php namespace Automattic\WooCommerce\EmailEditorVendor\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; } } email-editor/vendor-prefixed/packages/Sabberworm/CSS/Property/AtRule.php 0000777 00000001744 15252010675 0022302 0 ustar 00 <?php namespace Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Property; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Comment\Commentable; use Automattic\WooCommerce\EmailEditorVendor\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(); } email-editor/vendor-prefixed/packages/Sabberworm/CSS/Property/Charset.php 0000777 00000005651 15252010675 0022500 0 ustar 00 <?php namespace Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Property; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Comment\Comment; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\OutputFormat; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Position\Position; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Position\Positionable; use Automattic\WooCommerce\EmailEditorVendor\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; } } email-editor/vendor-prefixed/packages/Sabberworm/CSS/OutputFormatter.php 0000777 00000014666 15252010675 0022455 0 ustar 00 <?php namespace Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS; use Automattic\WooCommerce\EmailEditorVendor\Sabberworm\CSS\Comment\Commentable; use Automattic\WooCommerce\EmailEditorVendor\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()); } } email-editor/vendor-prefixed/packages/Sabberworm/CSS/OutputFormat.php 0000777 00000026425 15252010675 0021736 0 ustar 00 <?php namespace Automattic\WooCommerce\EmailEditorVendor\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; } } email-editor/vendor-prefixed/composer.json 0000777 00000001736 15252010675 0014763 0 ustar 00 { "name": "woocommerce/email-editor-lib", "description": "A package for hiding re-namespaced dependencies and executing them", "prefer-stable": true, "minimum-stability": "dev", "require": { "php": ">=7.4" }, "require-dev": { "pelago/emogrifier": "7.3.0" }, "config": { "platform": { "php": "7.4" } }, "scripts": { "post-install-cmd": [ "\"../../../../plugins/woocommerce/vendor/bin/mozart\" compose" ], "post-update-cmd": [ "\"../../../../plugins/woocommerce/vendor/bin/mozart\" compose" ] }, "extra": { "mozart": { "dep_namespace": "Automattic\\WooCommerce\\EmailEditorVendor\\", "dep_directory": "/packages/", "packages": [ "pelago/emogrifier" ], "excluded_packages": [ ], "override_autoload": { "thecodingmachine/safe": { "psr-4": { "Safe\\": "" } } }, "classmap_directory": "/classes/", "classmap_prefix": "EmailEditorVendor_", "delete_vendor_directories": true } } } email-editor/src/class-package.php 0000777 00000001334 15252010675 0013130 0 ustar 00 <?php /** * Email Editor Container class file. * * @package Automattic\WooCommerce\EmailEditor */ declare( strict_types = 1 ); namespace Automattic\WooCommerce\EmailEditor; defined( 'ABSPATH' ) || exit; /** * Main package class. */ class Package { /** * Version. * * @var string */ const VERSION = '0.1.0'; /** * Init the package. * * @return void */ public static function init() { Email_Editor_Container::init(); } /** * Return the version of the package. * * @return string */ public static function get_version() { return self::VERSION; } /** * Return the path to the package. * * @return string */ public static function get_path() { return dirname( __DIR__ ); } } email-editor/src/class-container.php 0000777 00000004474 15252010675 0013527 0 ustar 00 <?php /** * This file is part of the WooCommerce Email Editor package * * @package Automattic\WooCommerce\EmailEditor */ declare( strict_types = 1 ); namespace Automattic\WooCommerce\EmailEditor; /** * Class Container is a simple dependency injection container. * * @package Automattic\WooCommerce\EmailEditor */ class Container { /** * A list of registered services * * @var array<string, callable> $services */ protected array $services = array(); /** * A list of created instances * * @var array<string, object> $instances */ protected array $instances = array(); /** * Prevents deserialization of this class to avoid callback replacement attacks. * * @param array $data The serialized data. * @return void * @throws \Exception Always throws an exception to prevent deserialization. */ public function __unserialize( array $data ): void { throw new \Exception( 'Deserialization of Container is not allowed for security reasons.' ); } /** * The method for registering a new service. * * @param string $name The name of the service. * @param callable $callback The callable that will be used to create the service. * @return void * @phpstan-template T of object * @phpstan-param class-string<T> $name */ public function set( string $name, callable $callback ): void { $this->services[ $name ] = $callback; } /** * Method for getting a registered service. * * @param string $name The name of the service. * @return object The service instance. * @throws \Exception If the service is not found. * @phpstan-template T of object * @phpstan-param class-string<T> $name * @phpstan-return T */ public function get( string $name ): object { // Check if the service is already instantiated. if ( isset( $this->instances[ $name ] ) ) { /** * Instance. * * @var T $instance Instance of requested service. */ $instance = $this->instances[ $name ]; return $instance; } // Check if the service is registered. if ( ! isset( $this->services[ $name ] ) ) { throw new \Exception( esc_html( "Service not found: $name" ) ); } /** * Instance. * * @var T $instance Instance of requested service. */ $instance = $this->services[ $name ]( $this ); $this->instances[ $name ] = $instance; return $instance; } } email-editor/src/exceptions.php 0000777 00000005477 15252010675 0012627 0 ustar 00 <?php declare(strict_types = 1); // phpcs:ignoreFile PSR1.Classes.ClassDeclaration namespace Automattic\WooCommerce\EmailEditor; /** * Provides information for converting exceptions to HTTP responses. */ interface HttpAwareException { public function getHttpStatusCode(): int; } /** * Frames all exceptions ("$e instanceof Automattic\WooCommerce\EmailEditor\Exception"). */ abstract class Exception extends \Exception { /** @var string[] */ private $errors = []; final public function __construct(string $message = '', int $code = 0, ?\Throwable $previous = null) { parent::__construct($message, $code, $previous); } /** @return static */ public static function create(?\Throwable $previous = null) { return new static('', 0, $previous); } /** @return static */ public function withMessage(string $message) { $this->message = $message; return $this; } /** @return static */ public function withCode(int $code) { $this->code = $code; return $this; } /** * @param string[] $errors * @return static */ public function withErrors(array $errors) { $this->errors = $errors; return $this; } /** @return static */ public function withError(string $id, string $error) { $this->errors[$id] = $error; return $this; } /** * @return string[] */ public function getErrors(): array { return $this->errors; } } /** * USE: Generic runtime error. When possible, use a more specific exception instead. * API: 500 Server Error (not HTTP-aware) */ class RuntimeException extends Exception {} /** * USE: When wrong data VALUE is received. * API: 400 Bad Request */ class UnexpectedValueException extends RuntimeException implements HttpAwareException { public function getHttpStatusCode(): int { return 400; } } /** * USE: When an action is forbidden for given actor (although generally valid). * API: 403 Forbidden */ class AccessDeniedException extends UnexpectedValueException implements HttpAwareException { public function getHttpStatusCode(): int { return 403; } } /** * USE: When the main resource we're interested in doesn't exist. * API: 404 Not Found */ class NotFoundException extends UnexpectedValueException implements HttpAwareException { public function getHttpStatusCode(): int { return 404; } } /** * USE: When the main action produces conflict (i.e. duplicate key). * API: 409 Conflict */ class ConflictException extends UnexpectedValueException implements HttpAwareException { public function getHttpStatusCode(): int { return 409; } } /** * USE: An application state that should not occur. Can be subclassed for feature-specific exceptions. * API: 500 Server Error (not HTTP-aware) */ class InvalidStateException extends RuntimeException {} class NewsletterProcessingException extends Exception {} email-editor/src/Integrations/WooCommerce/Renderer/Blocks/class-product-price.php 0000777 00000015365 15252010675 0024176 0 ustar 00 <?php /** * This file is part of the WooCommerce Email Editor package. * * @package Automattic\WooCommerce\EmailEditor */ declare( strict_types = 1 ); namespace Automattic\WooCommerce\EmailEditor\Integrations\WooCommerce\Renderer\Blocks; use Automattic\WooCommerce\EmailEditor\Engine\Renderer\ContentRenderer\Rendering_Context; use Automattic\WooCommerce\EmailEditor\Integrations\Utils\Styles_Helper; use Automattic\WooCommerce\EmailEditor\Integrations\Utils\Table_Wrapper_Helper; /** * Renders a WooCommerce product price block for email. */ class Product_Price extends Abstract_Product_Block_Renderer { /** * Render the product price block content for email. * * @param string $block_content Block content. * @param array $parsed_block Parsed block. * @param Rendering_Context $rendering_context Rendering context. * @return string */ protected function render_content( string $block_content, array $parsed_block, Rendering_Context $rendering_context ): string { $product = $this->get_product_from_context( $parsed_block ); if ( ! $product ) { return ''; } $attributes = $parsed_block['attrs'] ?? array(); $price_content = $this->generate_price_html( $product, $attributes, $rendering_context ); return $this->apply_email_wrapper( $price_content, $parsed_block ); } /** * Generate clean price HTML from product data. * * @param \WC_Product $product Product object. * @param array $attributes Block attributes. * @param Rendering_Context $rendering_context Rendering context. * @return string */ private function generate_price_html( \WC_Product $product, array $attributes, Rendering_Context $rendering_context ): string { $price_html = $this->build_price_from_scratch( $product ); if ( empty( $price_html ) ) { return ''; } $price_styles = array( 'display' => 'block', 'margin' => '0', 'padding' => '0', 'font-family' => 'inherit', 'color' => 'inherit', 'text-decoration' => 'none', ); $custom_styles = Styles_Helper::get_block_styles( $attributes, $rendering_context, array( 'border', 'background-color', 'color', 'typography', 'spacing' ) ); $price_styles = array_merge( $price_styles, $custom_styles['declarations'] ); $style_attr = \WP_Style_Engine::compile_css( $price_styles, '' ); return sprintf( '<div class="wc-block-components-product-price" style="%s">%s</div>', esc_attr( $style_attr ), $price_html ); } /** * Build price HTML completely from scratch based on product type. * * @param \WC_Product $product Product object. * @return string */ private function build_price_from_scratch( \WC_Product $product ): string { $product_type = $product->get_type(); switch ( $product_type ) { case 'simple': case 'external': return $this->build_simple_product_price( $product ); case 'variable': // When the product does not have a correct type, the default will be used. if ( $product instanceof \WC_Product_Variable ) { return $this->build_variable_product_price( $product ); } return $this->build_simple_product_price( $product ); case 'grouped': // When the product does not have a correct type, the default will be used. if ( $product instanceof \WC_Product_Grouped ) { return $this->build_grouped_product_price( $product ); } return $this->build_simple_product_price( $product ); default: return $this->build_simple_product_price( $product ); } } /** * Build price HTML for simple products. * * @param \WC_Product $product Product object. * @return string */ private function build_simple_product_price( \WC_Product $product ): string { $regular_price = wc_get_price_to_display( $product, array( 'price' => (float) $product->get_regular_price() ) ); $sale_price = $product->get_sale_price() !== '' ? wc_get_price_to_display( $product, array( 'price' => (float) $product->get_sale_price() ) ) : ''; if ( empty( $regular_price ) ) { return ''; } if ( $product->is_on_sale() && '' !== $sale_price ) { return sprintf( '<del style="text-decoration: line-through; font-size: 0.9em; margin-right: 0.5em;">%s</del><span>%s</span>', wc_price( $regular_price, array( 'in_span' => false ) ), wc_price( $sale_price, array( 'in_span' => false ) ) ); } else { return sprintf( '<span>%s</span>', wc_price( $regular_price, array( 'in_span' => false ) ) ); } } /** * Build price HTML for variable products. * Uses the same logic as the editor: get_variation_price() methods. * * @param \WC_Product_Variable $product Variable product object. * @return string */ private function build_variable_product_price( \WC_Product_Variable $product ): string { $min_price = $product->get_variation_price( 'min', true ); $max_price = $product->get_variation_price( 'max', true ); return sprintf( '<span>%s — %s</span>', wc_price( (float) $min_price, array( 'in_span' => false ) ), wc_price( (float) $max_price, array( 'in_span' => false ) ) ); } /** * Build price HTML for grouped products. * * @param \WC_Product_Grouped $product Grouped product object. * @return string */ private function build_grouped_product_price( \WC_Product_Grouped $product ): string { $children = $product->get_children(); if ( empty( $children ) ) { return ''; } $prices = array(); foreach ( $children as $child_id ) { $child = wc_get_product( $child_id ); if ( $child && $child->get_price() !== '' ) { $prices[] = wc_get_price_to_display( $child, array( 'price' => (float) $child->get_price() ) ); } } if ( empty( $prices ) ) { return ''; } $min_price = min( $prices ); $max_price = max( $prices ); return sprintf( '<span>%s — %s</span>', wc_price( (float) $min_price, array( 'in_span' => false ) ), wc_price( (float) $max_price, array( 'in_span' => false ) ) ); } /** * Apply email-compatible table wrapper. * * @param string $price_html Price HTML. * @param array $parsed_block Parsed block. * @return string */ private function apply_email_wrapper( string $price_html, array $parsed_block ): string { $align = $parsed_block['attrs']['textAlign'] ?? 'left'; $wrapper_styles = array( 'border-collapse' => 'collapse', 'width' => '100%', ); $cell_styles = array( 'padding' => '5px 0', 'text-align' => $align, 'font-family' => 'inherit', ); $table_attrs = array( 'style' => \WP_Style_Engine::compile_css( $wrapper_styles, '' ), 'width' => '100%', ); $cell_attrs = array( 'class' => 'email-product-price-cell', 'style' => \WP_Style_Engine::compile_css( $cell_styles, '' ), 'align' => $align, ); return Table_Wrapper_Helper::render_table_wrapper( $price_html, $table_attrs, $cell_attrs ); } } email-editor/src/Integrations/WooCommerce/Renderer/Blocks/class-product-button.php 0000777 00000011224 15252010675 0024375 0 ustar 00 <?php /** * This file is part of the WooCommerce Email Editor package. * * @package Automattic\WooCommerce\EmailEditor */ declare( strict_types = 1 ); namespace Automattic\WooCommerce\EmailEditor\Integrations\WooCommerce\Renderer\Blocks; use Automattic\WooCommerce\EmailEditor\Engine\Renderer\ContentRenderer\Rendering_Context; use Automattic\WooCommerce\EmailEditor\Integrations\Utils\Styles_Helper; use Automattic\WooCommerce\EmailEditor\Integrations\Utils\Table_Wrapper_Helper; /** * Renders a WooCommerce product button block for email. */ class Product_Button extends Abstract_Product_Block_Renderer { /** * Get styles for the wrapper element. * * @param array $block_attributes Block attributes. * @param Rendering_Context $rendering_context Rendering context. * @return array */ private function get_wrapper_styles( array $block_attributes, Rendering_Context $rendering_context ): array { $block_styles = Styles_Helper::get_block_styles( $block_attributes, $rendering_context, array( 'border', 'background-color', 'color', 'typography', 'spacing' ) ); return Styles_Helper::extend_block_styles( $block_styles, array( 'word-break' => 'break-word', 'display' => 'block', ) ); } /** * Get styles for the button link element. * * @param array $block_attributes Block attributes. * @param Rendering_Context $rendering_context Rendering context. * @return array */ private function get_button_styles( array $block_attributes, Rendering_Context $rendering_context ): array { $block_styles = Styles_Helper::get_block_styles( $block_attributes, $rendering_context, array( 'color', 'typography' ) ); return Styles_Helper::extend_block_styles( $block_styles, array( 'display' => 'block', 'text-decoration' => 'none', 'width' => '100%', ) ); } /** * Render the product button block content for email. * * @param string $block_content Block content. * @param array $parsed_block Parsed block. * @param Rendering_Context $rendering_context Rendering context. * @return string */ protected function render_content( string $block_content, array $parsed_block, Rendering_Context $rendering_context ): string { $product = $this->get_product_from_context( $parsed_block ); if ( ! $product ) { return ''; } // Check if this is a cart-contents collection to customize button text and link. $collection = $parsed_block['context']['collection'] ?? ''; $is_cart_contents = 'woocommerce/product-collection/cart-contents' === $collection; if ( $is_cart_contents ) { // For cart contents, link to cart page instead of product page. $button_text = __( 'Finish checkout', 'woocommerce' ); $button_url = wc_get_cart_url(); } else { $button_text = $product->add_to_cart_text() ? $product->add_to_cart_text() : __( 'Add to cart', 'woocommerce' ); if ( $product->is_type( 'external' ) && $product instanceof \WC_Product_External ) { $external_url = $product->get_product_url(); $button_url = $external_url ? $external_url : $product->get_permalink(); } else { $button_url = $product->get_permalink(); } } $block_attributes = array_replace_recursive( array( 'textColor' => '#ffffff', 'backgroundColor' => '#000000', 'textAlign' => 'left', 'width' => '', 'style' => array( 'typography' => array( 'fontSize' => '16px', 'fontWeight' => 'bold', ), 'border' => array( 'radius' => '0', ), 'spacing' => array( 'padding' => '12px 24px', ), ), ), $parsed_block['attrs'] ?? array() ); $wrapper_styles = $this->get_wrapper_styles( $block_attributes, $rendering_context ); $button_styles = $this->get_button_styles( $block_attributes, $rendering_context ); $table_attrs = array( 'style' => 'width:' . ( $block_attributes['width'] ? '100%' : 'auto' ) . ';', 'align' => $block_attributes['textAlign'], ); $cell_attrs = array( 'class' => $wrapper_styles['classnames'], 'style' => $wrapper_styles['css'], 'align' => $block_attributes['textAlign'], 'valign' => 'middle', 'role' => 'presentation', ); $button_content = sprintf( '<a class="product-button-link %1$s" style="%2$s" href="%3$s" target="_blank">%4$s</a>', esc_attr( $button_styles['classnames'] ), esc_attr( $button_styles['css'] ), esc_url( $button_url ), esc_html( $button_text ) ); $button_html = Table_Wrapper_Helper::render_table_wrapper( $button_content, $table_attrs, $cell_attrs ); return Table_Wrapper_Helper::render_table_wrapper( $button_html, array( 'style' => 'width: 100%' ) ); } } email-editor/src/Integrations/WooCommerce/Renderer/Blocks/class-product-image.php 0000777 00000034773 15252010675 0024162 0 ustar 00 <?php /** * This file is part of the WooCommerce Email Editor package. * * @package Automattic\WooCommerce\EmailEditor */ declare( strict_types = 1 ); namespace Automattic\WooCommerce\EmailEditor\Integrations\WooCommerce\Renderer\Blocks; use Automattic\WooCommerce\EmailEditor\Engine\Renderer\ContentRenderer\Rendering_Context; use Automattic\WooCommerce\EmailEditor\Integrations\Utils\Dom_Document_Helper; use Automattic\WooCommerce\EmailEditor\Integrations\Utils\Styles_Helper; use Automattic\WooCommerce\EmailEditor\Integrations\Utils\Table_Wrapper_Helper; /** * Renders a WooCommerce product image block for email. */ class Product_Image extends Abstract_Product_Block_Renderer { /** * Render the product image block content for email. * * @param string $block_content Block content. * @param array $parsed_block Parsed block. * @param Rendering_Context $rendering_context Rendering context. * @return string */ protected function render_content( string $block_content, array $parsed_block, Rendering_Context $rendering_context ): string { $product = $this->get_product_from_context( $parsed_block ); if ( ! $product ) { return ''; } $attributes = $this->parse_attributes( $parsed_block['attrs'] ?? array() ); $image_data = $this->get_product_image_data( $product, $attributes ); if ( ! $image_data ) { return ''; } $parsed_block = $this->add_image_size_when_missing( $parsed_block, $rendering_context ); $attributes = $this->parse_attributes( $parsed_block['attrs'] ?? array() ); $image_html = $this->build_image_html( $image_data, $attributes, $rendering_context ); $inner_blocks = $this->process_inner_blocks( $parsed_block, $product, $rendering_context ); $combined_content = $this->create_overlay_structure( $image_html, $inner_blocks['badges'], $inner_blocks['other_content'], $inner_blocks['badge_alignment'], $product, $attributes['showProductLink'] ); return $this->apply_email_wrapper( $combined_content, $parsed_block, $rendering_context ); } /** * Process inner blocks (like sale badges) from block content. * Handles special positioning for email compatibility. * * @param array $parsed_block Parsed block. * @param \WC_Product $product Product object. * @param Rendering_Context $rendering_context Rendering context. * @return array Array with 'badges' and 'other_content' keys */ private function process_inner_blocks( array $parsed_block, \WC_Product $product, Rendering_Context $rendering_context ): array { $badges = ''; $other_content = ''; $badge_alignment = 'left'; if ( ! empty( $parsed_block['innerBlocks'] ) ) { foreach ( $parsed_block['innerBlocks'] as $inner_block ) { $inner_block['context'] = $inner_block['context'] ?? array(); $inner_block['context']['postId'] = $product->get_id(); if ( 'woocommerce/product-sale-badge' === $inner_block['blockName'] ) { $badges .= $this->render_overlay_badge( $inner_block, $product, $rendering_context ); $badge_alignment = $inner_block['attrs']['align'] ?? 'left'; } else { $other_content .= render_block( $inner_block ); } } } return array( 'badges' => $badges, 'other_content' => $other_content, 'badge_alignment' => $badge_alignment, ); } /** * Render a sale badge with email-compatible overlay positioning. * * @param array $badge_block Badge block data. * @param \WC_Product $product Product object. * @param Rendering_Context $rendering_context Rendering context. * @return string */ private function render_overlay_badge( array $badge_block, \WC_Product $product, Rendering_Context $rendering_context ): string { if ( ! $product->is_on_sale() ) { return ''; } $sale_text = apply_filters( 'woocommerce_sale_badge_text', __( 'Sale', 'woocommerce' ), $product ); $badge_attributes = array_replace_recursive( array( 'textColor' => '#43454b', 'backgroundColor' => '#fff', 'style' => array( 'border' => array( 'width' => '1px', 'radius' => '4px', 'color' => '#43454b', ), 'spacing' => array( 'padding' => '4px 12px', ), 'typography' => array( 'fontSize' => '14px', 'fontWeight' => '600', 'textTransform' => 'uppercase', 'lineHeight' => '1.5', ), ), ), wp_parse_args( $badge_block['attrs'] ?? array() ) ); $block_styles = Styles_Helper::get_block_styles( $badge_attributes, $rendering_context, array( 'border', 'background-color', 'color', 'typography', 'spacing' ) ); $additional_styles = array( 'display' => 'inline-block', 'width' => 'fit-content', 'box-sizing' => 'border-box', ); $final_styles = Styles_Helper::extend_block_styles( $block_styles, $additional_styles ); return sprintf( '<span class="wc-block-components-product-sale-badge__text" style="%s">%s</span>', esc_attr( $final_styles['css'] ), esc_html( $sale_text ) ); } /** * Create overlay structure for email compatibility. * Uses Faux Absolute Position with badge-below fallback for better cross-client support. * * @param string $image_html Image HTML. * @param string $badges_html Badges HTML. * @param string $other_content Other inner content. * @param string $badge_alignment Badge alignment. * @param \WC_Product|null $product Product object for link. * @param bool $show_product_link Whether to show product link. * @return string */ private function create_overlay_structure( string $image_html, string $badges_html, string $other_content, string $badge_alignment, ?\WC_Product $product = null, bool $show_product_link = false ): string { if ( empty( $badges_html ) ) { $linked_image_html = $image_html; if ( $show_product_link && $product ) { $linked_image_html = $this->wrap_with_link( $image_html, $product ); } return $linked_image_html . $other_content; } $image_width = $this->extract_image_width( $image_html ); $image_height = $this->extract_image_height( $image_html ); $linked_image_html = $image_html; if ( $show_product_link && $product ) { $linked_image_html = $this->wrap_with_link( $image_html, $product ); } $vml_side = ( 'left' === $badge_alignment ) ? 'left' : 'right'; $overlay_html = sprintf( '<table cellpadding="0" cellspacing="0" border="0" style="width: %dpx; height: %dpx; table-layout: fixed;"> <tr> <td style="font-size: 0; line-height: 0; padding: 0; height: %dpx; width: %dpx;"> <div style="max-height:0; position:relative; opacity:0.999;"> <!--[if mso]> <v:rect xmlns:v="urn:schemas-microsoft-com:vml" stroked="false" filled="false" style="mso-width-percent: 1000; position:absolute; top:16px; ' . esc_attr( $vml_side ) . ':16px;"> <v:textbox inset="0,0,0,0"> <![endif]--> <div style="padding: 12px; box-sizing: border-box; display: inline-block; width: 100%%; text-align: %s;"> %s </div> <!--[if mso]> </v:textbox> </v:rect> <![endif]--> </div> %s </td> </tr> </table>%s', $image_width, $image_height, $image_height, $image_width, $badge_alignment, $badges_html, $linked_image_html, $other_content ); return $overlay_html; } /** * Extract image width from HTML for positioning calculations. * * @param string $image_html Image HTML. * @return float Image width in pixels. */ private function extract_image_width( string $image_html ): float { $width = ( new Dom_Document_Helper( $image_html ) )->get_attribute_value_by_tag_name( 'img', 'width' ) ?? ''; if ( $width ) { return Styles_Helper::parse_value( $width ); } return 300; } /** * Extract image height from HTML for positioning calculations. * * @param string $image_html Image HTML. * @return float Image height in pixels. */ private function extract_image_height( string $image_html ): float { $height = ( new Dom_Document_Helper( $image_html ) )->get_attribute_value_by_tag_name( 'img', 'height' ) ?? ''; if ( $height ) { return Styles_Helper::parse_value( $height ); } return 300; } /** * When the width is not set, it's important to get it for the image to be displayed correctly. * Based on the email Image renderer logic. * * @param array $parsed_block Parsed block. * @param Rendering_Context $rendering_context Rendering context. * @return array */ private function add_image_size_when_missing( array $parsed_block, Rendering_Context $rendering_context ): array { if ( isset( $parsed_block['attrs']['width'] ) ) { return $parsed_block; } if ( ! isset( $parsed_block['email_attrs']['width'] ) ) { $parsed_block['attrs']['width'] = '100%'; return $parsed_block; } $parsed_block['attrs']['width'] = $rendering_context->get_layout_width_without_padding(); return $parsed_block; } /** * Parse block attributes with defaults. * * @param array $attributes Block attributes. * @return array */ private function parse_attributes( array $attributes ): array { return wp_parse_args( $attributes, array( 'showProductLink' => true, 'imageSizing' => 'single', 'scale' => 'cover', 'showSaleBadge' => false, 'saleBadgeAlign' => 'right', ) ); } /** * Get product image data. * * @param \WC_Product $product Product object. * @param array $attributes Parsed attributes. * @return array|null */ private function get_product_image_data( \WC_Product $product, array $attributes ): ?array { $image_size = 'single' === $attributes['imageSizing'] ? 'woocommerce_single' : 'woocommerce_thumbnail'; $image_id = (int) $product->get_image_id(); if ( ! $image_id ) { $placeholder = wc_placeholder_img_src( $image_size ); return array( 'url' => $placeholder, 'alt' => $product->get_name(), 'width' => 300, 'height' => 300, ); } $image_url = wp_get_attachment_image_url( $image_id, $image_size ); if ( ! $image_url ) { return null; } $alt_text = get_post_meta( $image_id, '_wp_attachment_image_alt', true ); $image_meta = wp_get_attachment_metadata( $image_id ); return array( 'url' => $image_url, 'alt' => $alt_text ? $alt_text : $product->get_name(), 'width' => $image_meta['width'] ?? 300, 'height' => $image_meta['height'] ?? 300, ); } /** * Build email-compatible image HTML. * * @param array $image_data Image data. * @param array $attributes Block attributes. * @param Rendering_Context $rendering_context Rendering context. * @return string */ private function build_image_html( array $image_data, array $attributes, Rendering_Context $rendering_context ): string { $style_parts = array( 'max-width' => '100%', 'height' => 'auto', 'display' => 'block', ); if ( ! empty( $attributes['scale'] ) ) { $style_parts['object-fit'] = $attributes['scale']; } if ( ! empty( $attributes['width'] ) ) { $style_parts['width'] = $attributes['width']; } if ( ! empty( $attributes['height'] ) ) { $style_parts['height'] = $attributes['height']; } if ( ! empty( $attributes['aspectRatio'] ) ) { $style_parts['aspect-ratio'] = $attributes['aspectRatio']; } $width = ! empty( $attributes['width'] ) ? Styles_Helper::parse_value( $attributes['width'] ) : $image_data['width']; $layout_width = Styles_Helper::parse_value( $rendering_context->get_layout_width_without_padding() ); if ( $width > $layout_width ) { $width = $layout_width; $aspect_ratio = $image_data['height'] / $image_data['width']; $attributes['height'] = round( $width * $aspect_ratio ) . 'px'; } $height = $image_data['height']; if ( ! empty( $attributes['height'] ) ) { $height = Styles_Helper::parse_value( $attributes['height'] ); } elseif ( ! empty( $attributes['width'] ) && $image_data['width'] > 0 ) { $aspect_ratio = $image_data['height'] / $image_data['width']; $height = round( $width * $aspect_ratio ); } return sprintf( '<img class="email-editor-product-image skip-lazy" data-skip-lazy="1" loading="eager" decoding="auto" src="%s" alt="%s" style="%s" width="%d" height="%d" />', esc_url( $image_data['url'] ), esc_attr( $image_data['alt'] ), esc_attr( \WP_Style_Engine::compile_css( $style_parts, '' ) ), $width, $height ); } /** * Wrap image with product link. * * @param string $image_html Image HTML. * @param \WC_Product $product Product object. * @return string */ private function wrap_with_link( string $image_html, \WC_Product $product ): string { $product_url = $product->get_permalink(); return sprintf( '<a href="%s" style="display: block; text-decoration: none;">%s</a>', esc_url( $product_url ), $image_html ); } /** * Apply email-compatible table wrapper (similar to Image renderer). * * @param string $image_html Image HTML. * @param array $parsed_block Parsed block. * @param Rendering_Context $rendering_context Rendering context. * @return string */ private function apply_email_wrapper( string $image_html, array $parsed_block, Rendering_Context $rendering_context ): string { $width = $parsed_block['attrs']['width'] ?? ''; $wrapper_width = ( $width && '100%' !== $width ) ? $width : 'auto'; $image_height = $this->extract_image_height( $image_html ) . 'px'; $wrapper_styles = array( 'border-collapse' => 'separate', 'width' => $wrapper_width, ); $cell_styles = array( 'overflow' => 'hidden', 'vertical-align' => 'top', ); $align = $parsed_block['attrs']['align'] ?? 'left'; $cell_styles['text-align'] = $align; $outer_table_attrs = array( 'style' => \WP_Style_Engine::compile_css( array( 'border-collapse' => 'collapse', 'border-spacing' => '0px', 'width' => '100%', 'height' => $image_height, ), '' ), 'width' => '100%', ); $outer_cell_attrs = array( 'align' => $align, ); $inner_table_attrs = array( 'style' => \WP_Style_Engine::compile_css( $wrapper_styles, '' ), 'width' => $wrapper_width, 'height' => $image_height, ); $inner_cell_attrs = array( 'style' => \WP_Style_Engine::compile_css( $cell_styles, '' ), ); $inner_content = Table_Wrapper_Helper::render_table_wrapper( $image_html, $inner_table_attrs, $inner_cell_attrs ); return Table_Wrapper_Helper::render_table_wrapper( $inner_content, $outer_table_attrs, $outer_cell_attrs ); } } email-editor/src/Integrations/WooCommerce/Renderer/Blocks/class-product-sale-badge.php 0000777 00000011306 15252010675 0025047 0 ustar 00 <?php /** * This file is part of the WooCommerce Email Editor package. * * @package Automattic\WooCommerce\EmailEditor */ declare( strict_types = 1 ); namespace Automattic\WooCommerce\EmailEditor\Integrations\WooCommerce\Renderer\Blocks; use Automattic\WooCommerce\EmailEditor\Engine\Renderer\ContentRenderer\Rendering_Context; use Automattic\WooCommerce\EmailEditor\Integrations\Utils\Styles_Helper; use Automattic\WooCommerce\EmailEditor\Integrations\Utils\Table_Wrapper_Helper; /** * Renders a WooCommerce product sale badge block for email. */ class Product_Sale_Badge extends Abstract_Product_Block_Renderer { /** * Render the product sale badge block content for email. * * @param string $block_content Block content. * @param array $parsed_block Parsed block. * @param Rendering_Context $rendering_context Rendering context. * @return string */ protected function render_content( string $block_content, array $parsed_block, Rendering_Context $rendering_context ): string { $product = $this->get_product_from_context( $parsed_block ); if ( ! $product ) { return ''; } if ( ! $product->is_on_sale() ) { return ''; } $attributes = $parsed_block['attrs'] ?? array(); /** * Filters the product sale badge text. * * @hook woocommerce_sale_badge_text * @since 10.0.0 * * @param string $sale_text The sale badge text. * @param \WC_Product $product The product object. * @return string The filtered sale badge text. */ $sale_text = apply_filters( 'woocommerce_sale_badge_text', __( 'Sale', 'woocommerce' ), $product ); $badge_html = $this->build_badge_html( $sale_text, $attributes, $rendering_context ); return $this->apply_email_wrapper( $badge_html, $parsed_block ); } /** * Build email-compatible badge HTML. * * @param string $sale_text Sale badge text. * @param array $attributes Block attributes. * @param Rendering_Context $rendering_context Rendering context. * @return string */ private function build_badge_html( string $sale_text, array $attributes, Rendering_Context $rendering_context ): string { $align = $attributes['align'] ?? 'left'; $position_style = $this->get_position_style( $align ); $badge_styles = array_merge( array( 'font-size' => '0.875em', 'padding' => '0.25em 0.75em', 'display' => 'inline-block', 'width' => 'fit-content', 'border' => '1px solid #43454b', 'border-radius' => '4px', 'box-sizing' => 'border-box', 'color' => '#43454b', 'background' => '#fff', 'text-align' => 'center', 'text-transform' => 'uppercase', 'font-weight' => '600', 'z-index' => '9', 'position' => 'static', ), $position_style ); $custom_styles = Styles_Helper::get_block_styles( $attributes, $rendering_context, array( 'border', 'background-color', 'color', 'typography', 'spacing' ) ); $style_attr = \WP_Style_Engine::compile_css( array_merge( $badge_styles, $custom_styles['declarations'] ?? array() ), '' ); return sprintf( '<span class="wc-block-components-product-sale-badge__text" style="%s">%s</span>', esc_attr( $style_attr ), esc_html( $sale_text ) ); } /** * Get positioning styles based on alignment. * * @param string $align Alignment value. * @return array */ private function get_position_style( string $align ): array { switch ( $align ) { case 'left': return array( 'text-align' => 'left', 'margin-right' => 'auto', ); case 'center': return array( 'text-align' => 'center', 'margin-left' => 'auto', 'margin-right' => 'auto', ); case 'right': return array( 'text-align' => 'right', 'margin-left' => 'auto', ); default: return array( 'text-align' => 'left', ); } } /** * Apply email-compatible table wrapper. * * @param string $badge_html Badge HTML. * @param array $parsed_block Parsed block. * @return string */ private function apply_email_wrapper( string $badge_html, array $parsed_block ): string { $align = $parsed_block['attrs']['align'] ?? 'left'; $wrapper_styles = array( 'border-collapse' => 'collapse', 'width' => '100%', ); $cell_styles = array( 'padding' => '5px 0', 'text-align' => $align, ); $table_attrs = array( 'style' => \WP_Style_Engine::compile_css( $wrapper_styles, '' ), 'width' => '100%', ); $cell_attrs = array( 'class' => 'email-product-sale-badge-cell', 'style' => \WP_Style_Engine::compile_css( $cell_styles, '' ), 'align' => $align, ); return Table_Wrapper_Helper::render_table_wrapper( $badge_html, $table_attrs, $cell_attrs ); } } email-editor/src/Integrations/WooCommerce/Renderer/Blocks/class-product-collection.php 0000777 00000047412 15252010675 0025225 0 ustar 00 <?php /** * This file is part of the WooCommerce Email Editor package * * @package Automattic\WooCommerce\EmailEditor */ declare( strict_types = 1 ); namespace Automattic\WooCommerce\EmailEditor\Integrations\WooCommerce\Renderer\Blocks; use Automattic\WooCommerce\EmailEditor\Engine\Renderer\ContentRenderer\Rendering_Context; use WP_Query; /** * Renders a product collection block for email. */ class Product_Collection extends Abstract_Product_Block_Renderer { /** * Render the product collection block content for email. * * @param string $block_content Block content. * @param array $parsed_block Parsed block. * @param Rendering_Context $rendering_context Rendering context. * @return string */ protected function render_content( string $block_content, array $parsed_block, Rendering_Context $rendering_context ): string { // Create a query for the Product Collection block. $query = $this->prepare_and_execute_query( $parsed_block, $rendering_context ); // Get collection type to pass to child blocks. $collection_type = $parsed_block['attrs']['collection'] ?? ''; $content = ''; foreach ( $parsed_block['innerBlocks'] as $inner_block ) { switch ( $inner_block['blockName'] ) { case 'woocommerce/product-template': $content .= $this->render_product_template( $inner_block, $query, $collection_type ); break; default: $content .= render_block( $inner_block ); break; } } wp_reset_postdata(); return $content; } /** * Render the product template block. * * @param array $inner_block Inner block data. * @param \WP_Query $query WP_Query object. * @param string $collection_type Collection type identifier. * @return string */ private function render_product_template( array $inner_block, \WP_Query $query, string $collection_type ): string { if ( ! $query->have_posts() ) { return $this->render_no_results_message(); } $posts = $query->get_posts(); $total_count = count( $posts ); if ( 0 === $total_count ) { return $this->render_no_results_message(); } $products = array_filter( array_map( function ( $post ) { return $post instanceof \WP_Post ? wc_get_product( $post->ID ) : null; }, $posts ) ); return $this->render_product_grid( $products, $inner_block, $collection_type ); } /** * Render product grid using HTML table structure for email compatibility. * * @param array $products Array of WC_Product objects. * @param array $inner_block Inner block data. * @param string $collection_type Collection type identifier. * @return string */ private function render_product_grid( array $products, array $inner_block, string $collection_type ): string { // We start with supporting 1 product per row. $content = ''; foreach ( $products as $product ) { $content .= $this->add_spacer( $this->render_product_content( $product, $inner_block, $collection_type ), $inner_block['email_attrs'] ?? array() ); } return $content; } /** * Render default product content when no inner blocks are present. * * @param \WC_Product|null $product Product object. * @param array $template_block Inner block data. * @param string $collection_type Collection type identifier. * @return string */ private function render_product_content( ?\WC_Product $product, array $template_block, string $collection_type ): string { $content = ''; if ( ! $product ) { return $content; } foreach ( $template_block['innerBlocks'] as $inner_block ) { switch ( $inner_block['blockName'] ) { case 'woocommerce/product-price': case 'woocommerce/product-button': case 'woocommerce/product-sale-badge': case 'woocommerce/product-image': $inner_block['context'] = $inner_block['context'] ?? array(); $inner_block['context']['postId'] = $product->get_id(); $inner_block['context']['collection'] = $collection_type; $content .= render_block( $inner_block ); break; case 'core/post-title': global $post; $original_post = $post; $original_global_product = $GLOBALS['product'] ?? null; $product_post = get_post( $product->get_id() ); $post = $product_post; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited $GLOBALS['product'] = $product; $inner_block['context'] = $inner_block['context'] ?? array(); $inner_block['context']['postId'] = $product->get_id(); $content .= render_block( $inner_block ); $post = $original_post; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited $GLOBALS['product'] = $original_global_product; break; default: break; } } return $content; } /** * Prepare and execute a query for the Product Collection block using the original QueryBuilder. * * @param array $parsed_block Parsed block data. * @param Rendering_Context $rendering_context Rendering context. * @return WP_Query */ private function prepare_and_execute_query( array $parsed_block, Rendering_Context $rendering_context ): WP_Query { $collection = $parsed_block['attrs']['collection'] ?? ''; $query_attrs = $parsed_block['attrs']['query'] ?? array(); // Build a direct WP_Query for email rendering (not using ProductCollection QueryBuilder). // The QueryBuilder is designed for REST/frontend context, not email rendering. $query_args = array( 'post_type' => 'product', 'post_status' => 'publish', 'posts_per_page' => (int) ( $query_attrs['perPage'] ?? 9 ), 'orderby' => sanitize_key( $query_attrs['orderBy'] ?? 'menu_order' ), 'order' => sanitize_key( $query_attrs['order'] ?? 'asc' ), 'meta_query' => array(), // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query 'tax_query' => array(), // phpcs:ignore WordPress.DB.SlowDBQuery ); // Handle search. if ( ! empty( $query_attrs['search'] ) ) { $query_args['s'] = sanitize_text_field( (string) $query_attrs['search'] ); } // Handle offset. if ( isset( $query_attrs['offset'] ) ) { $query_args['offset'] = (int) $query_attrs['offset']; } // Handle exclusions. if ( isset( $query_attrs['exclude'] ) && is_array( $query_attrs['exclude'] ) ) { $query_args['post__not_in'] = array_map( static function ( $id ) { return is_numeric( $id ) ? (int) $id : 0; }, $query_attrs['exclude'] ); } // Handle handpicked products. if ( ! empty( $query_attrs['woocommerceHandPickedProducts'] ) ) { $query_args['post__in'] = array_map( static function ( $id ) { return is_numeric( $id ) ? (int) $id : 0; }, $query_attrs['woocommerceHandPickedProducts'] ); $query_args['orderby'] = 'post__in'; } // Handle featured products - use the WooCommerce way. $is_featured = $query_attrs['featured'] ?? false; if ( 'woocommerce/product-collection/featured' === $collection || $is_featured ) { // Use WooCommerce's built-in function to get featured products query. $featured_query = wc_get_product_visibility_term_ids(); if ( isset( $featured_query['featured'] ) ) { $query_args['tax_query'][] = array( 'taxonomy' => 'product_visibility', 'field' => 'term_taxonomy_id', 'terms' => array( (int) $featured_query['featured'] ), 'operator' => 'IN', ); } } // Handle on-sale products. $is_on_sale = $query_attrs['woocommerceOnSale'] ?? false; if ( 'woocommerce/product-collection/on-sale' === $collection || $is_on_sale ) { $query_args['meta_query'][] = array( 'relation' => 'OR', array( 'key' => '_sale_price', 'value' => '', 'compare' => '!=', ), ); } // Handle stock status (only if not all statuses are selected). $stock_status = $query_attrs['woocommerceStockStatus'] ?? array(); if ( ! empty( $stock_status ) && ! $this->is_all_stock_statuses( $stock_status ) ) { $query_args['meta_query'][] = array( 'key' => '_stock_status', 'value' => $stock_status, 'compare' => 'IN', ); } // Handle taxonomies (categories, tags, etc.). if ( ! empty( $query_attrs['taxQuery'] ) ) { $tax_queries = $this->build_tax_query( $query_attrs['taxQuery'] ); $query_args['tax_query'] = array_merge( $query_args['tax_query'], $tax_queries ); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query } // Handle product attributes. if ( ! empty( $query_attrs['woocommerceAttributes'] ) ) { $attribute_queries = $this->build_attribute_query( $query_attrs['woocommerceAttributes'] ); $query_args['tax_query'] = array_merge( $query_args['tax_query'], $attribute_queries ); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query } // Handle special collections: upsells, cross-sells, related, cart-contents. $product_ids_to_include = $this->get_collection_specific_product_ids( $collection, $parsed_block, $rendering_context ); if ( ! empty( $product_ids_to_include ) ) { $query_args['post__in'] = $product_ids_to_include; } // Set tax_query relation if multiple tax queries. if ( count( $query_args['tax_query'] ) > 1 ) { $query_args['tax_query']['relation'] = 'AND'; } $wp_query = new WP_Query( $query_args ); return $wp_query; } /** * Check if all stock statuses are selected (meaning no filtering needed). * * @param array $stock_status Stock status values from block attributes. * @return bool */ private function is_all_stock_statuses( array $stock_status ): bool { if ( empty( $stock_status ) ) { return true; // Empty means all statuses. } $all_stock_statuses = array_keys( wc_get_product_stock_status_options() ); return count( $stock_status ) === count( $all_stock_statuses ) && count( array_diff( $stock_status, $all_stock_statuses ) ) === 0 && count( array_diff( $all_stock_statuses, $stock_status ) ) === 0; } /** * Build tax query from taxQuery block attributes. * * @param array $tax_query_input Tax query input from block attributes. * @return array */ private function build_tax_query( array $tax_query_input ): array { $tax_queries = array(); if ( empty( $tax_query_input ) ) { return $tax_queries; } $first_key = array_key_first( $tax_query_input ); // If not a numeric array of clauses, assume object map: { taxonomy => [termIds] }. if ( ! is_int( $first_key ) ) { foreach ( $tax_query_input as $taxonomy => $terms ) { if ( ! empty( $terms ) ) { $tax_queries[] = array( 'taxonomy' => $taxonomy, 'field' => 'term_id', 'terms' => array_map( static function ( $id ) { return is_numeric( $id ) ? (int) $id : 0; }, (array) $terms ), ); } } } else { $tax_queries = $tax_query_input; } return $tax_queries; } /** * Build attribute query from woocommerceAttributes block attributes. * * @param array $attributes Attribute filters from block attributes. * @return array */ private function build_attribute_query( array $attributes ): array { $attribute_queries = array(); foreach ( $attributes as $attribute ) { if ( ! empty( $attribute['taxonomy'] ) && ! empty( $attribute['termId'] ) ) { $attribute_queries[] = array( 'taxonomy' => $attribute['taxonomy'], 'field' => 'term_id', 'terms' => array( (int) $attribute['termId'] ), ); } } return $attribute_queries; } /** * Get specific product IDs for collection types that need them (upsell, cross-sell, related, cart-contents). * * @param string $collection Collection type. * @param array $parsed_block Parsed block data. * @param Rendering_Context $rendering_context Rendering context. * @return array Array of product IDs or empty array. */ private function get_collection_specific_product_ids( string $collection, array $parsed_block, Rendering_Context $rendering_context ): array { switch ( $collection ) { case 'woocommerce/product-collection/upsells': return $this->get_upsell_product_ids( $parsed_block ); case 'woocommerce/product-collection/cross-sells': return $this->get_cross_sell_product_ids( $parsed_block ); case 'woocommerce/product-collection/related': return $this->get_related_product_ids( $parsed_block ); case 'woocommerce/product-collection/cart-contents': return $this->get_cart_contents_product_ids( $parsed_block, $rendering_context ); default: return array(); } } /** * Get upsell product IDs. * * @param array $parsed_block Parsed block data. * @return array Array of upsell product IDs. */ private function get_upsell_product_ids( array $parsed_block ): array { $product_references = $this->get_product_references_for_collection( $parsed_block ); if ( empty( $product_references ) ) { return array( -1 ); // Return -1 to ensure no products are found. } $products = array_filter( array_map( 'wc_get_product', $product_references ) ); if ( empty( $products ) ) { return array( -1 ); } $all_upsells = array(); foreach ( $products as $product ) { $all_upsells = array_merge( $all_upsells, $product->get_upsell_ids() ); } // Remove duplicates and product references (don't show what's already in context). $unique_upsells = array_unique( $all_upsells ); $upsells = array_diff( $unique_upsells, $product_references ); return ! empty( $upsells ) ? $upsells : array( -1 ); } /** * Get cross-sell product IDs. * * @param array $parsed_block Parsed block data. * @return array Array of cross-sell product IDs. */ private function get_cross_sell_product_ids( array $parsed_block ): array { $product_references = $this->get_product_references_for_collection( $parsed_block ); if ( empty( $product_references ) ) { return array( -1 ); // Return -1 to ensure no products are found. } $products = array_filter( array_map( 'wc_get_product', $product_references ) ); if ( empty( $products ) ) { return array( -1 ); } $product_ids = array_map( function ( $product ) { return $product->get_id(); }, $products ); $all_cross_sells = array(); foreach ( $products as $product ) { $all_cross_sells = array_merge( $all_cross_sells, $product->get_cross_sell_ids() ); } // Remove duplicates and product references (don't show what's already in context). $unique_cross_sells = array_unique( $all_cross_sells ); $cross_sells = array_diff( $unique_cross_sells, $product_ids ); return ! empty( $cross_sells ) ? $cross_sells : array( -1 ); } /** * Get related product IDs. * * @param array $parsed_block Parsed block data. * @return array Array of related product IDs. */ private function get_related_product_ids( array $parsed_block ): array { $product_references = $this->get_product_references_for_collection( $parsed_block ); if ( empty( $product_references ) ) { return array( -1 ); // Return -1 to ensure no products are found. } // For related products, we only use the first product reference. $product_reference = $product_references[0]; if ( empty( $product_reference ) ) { return array( -1 ); } // Get related products using WooCommerce's built-in function. $related_ids = wc_get_related_products( $product_reference, 100 ); return ! empty( $related_ids ) ? $related_ids : array( -1 ); } /** * Get product references for collections (handles different contexts). * * @param array $parsed_block Parsed block data. * @return array Array of product IDs or empty array. */ private function get_product_references_for_collection( array $parsed_block ): array { $query_attrs = $parsed_block['attrs']['query'] ?? array(); $product_references = array(); // First try to get from productReference in query attributes. if ( ! empty( $query_attrs['productReference'] ) ) { $product_references = array( (int) $query_attrs['productReference'] ); } // If no product reference found, try to get from global context. if ( empty( $product_references ) ) { global $product; if ( $product && is_a( $product, 'WC_Product' ) ) { $product_references = array( $product->get_id() ); } } // In email context, we might need additional context sources. // This could be extended based on email type (order confirmation, etc.). return $product_references; } /** * Get cart contents product IDs for email rendering. * * @param array $parsed_block Parsed block data. * @param Rendering_Context $rendering_context Rendering context. * @return array Array of cart product IDs or sample products for preview. */ private function get_cart_contents_product_ids( array $parsed_block, Rendering_Context $rendering_context ): array { // Try to get cart product IDs from the user's cart using user ID or email. $cart_product_ids = $this->get_user_cart_product_ids_from_context( $rendering_context ); if ( ! empty( $cart_product_ids ) ) { return $cart_product_ids; } // For preview emails, show sample products so users can see what the email will look like. if ( $rendering_context->get( 'is_user_preview', false ) ) { return $this->get_sample_product_ids_for_preview(); } // For real emails with empty cart, return -1 to ensure no products are shown. return array( -1 ); } /** * Get cart product IDs from rendering context using user ID or email. * * @param Rendering_Context $rendering_context Rendering context. * @return array Array of product IDs from the user's cart. */ private function get_user_cart_product_ids_from_context( Rendering_Context $rendering_context ): array { $user_id = $rendering_context->get_user_id(); $email = $rendering_context->get_recipient_email(); // Use shared utility if available (WooCommerce 10.4+). if ( class_exists( '\Automattic\WooCommerce\Blocks\Utils\CartCheckoutUtils' ) && method_exists( '\Automattic\WooCommerce\Blocks\Utils\CartCheckoutUtils', 'get_cart_product_ids_for_user' ) ) { // @phpstan-ignore-next-line - Method exists in newer WooCommerce versions. return \Automattic\WooCommerce\Blocks\Utils\CartCheckoutUtils::get_cart_product_ids_for_user( $user_id, $email ); } // Fallback: Get user ID from email if needed, then fetch cart. if ( ! $user_id && $email ) { $user = get_user_by( 'email', $email ); if ( $user ) { $user_id = $user->ID; } } if ( ! $user_id ) { return array(); } // Fallback implementation for older WooCommerce versions. $cart_data = get_user_meta( $user_id, '_woocommerce_persistent_cart_' . get_current_blog_id(), true ); if ( ! is_array( $cart_data ) || empty( $cart_data ) || ! isset( $cart_data['cart'] ) || ! is_array( $cart_data['cart'] ) ) { return array(); } $product_ids = array(); foreach ( $cart_data['cart'] as $cart_item ) { if ( is_array( $cart_item ) && isset( $cart_item['product_id'] ) && is_numeric( $cart_item['product_id'] ) ) { $product_ids[] = (int) $cart_item['product_id']; } } return array_unique( $product_ids ); } /** * Get sample product IDs for preview emails. * This ensures that preview emails show representative content even when the cart is empty. * * @return array Array of sample product IDs. */ private function get_sample_product_ids_for_preview(): array { $query = new WP_Query( array( 'post_type' => 'product', 'post_status' => 'publish', 'posts_per_page' => 3, 'orderby' => 'date', 'order' => 'DESC', 'fields' => 'ids', ) ); if ( ! empty( $query->posts ) && is_array( $query->posts ) ) { return array_map( static function ( $id ) { return is_numeric( $id ) ? (int) $id : 0; }, $query->posts ); } return array( -1 ); } /** * Render a no results message. * * @return string */ private function render_no_results_message(): string { return sprintf( '<div style="text-align: center; padding: 20px; color: #666;">%s</div>', esc_html__( 'No products found.', 'woocommerce' ) ); } } email-editor/src/Integrations/WooCommerce/Renderer/Blocks/class-abstract-product-block-renderer.php 0000777 00000002320 15252010675 0027556 0 ustar 00 <?php /** * This file is part of the WooCommerce Email Editor package * * @package Automattic\WooCommerce\EmailEditor */ declare( strict_types = 1 ); namespace Automattic\WooCommerce\EmailEditor\Integrations\WooCommerce\Renderer\Blocks; use Automattic\WooCommerce\EmailEditor\Engine\Renderer\ContentRenderer\Block_Renderer; use Automattic\WooCommerce\EmailEditor\Integrations\Core\Renderer\Blocks\Abstract_Block_Renderer; /** * Shared functionality for block renderers. */ abstract class Abstract_Product_Block_Renderer extends Abstract_Block_Renderer implements Block_Renderer { /** * Get product from block context. * * @param array $parsed_block Parsed block. * @return \WC_Product|null */ protected function get_product_from_context( array $parsed_block ): ?\WC_Product { $post_id = $parsed_block['context']['postId'] ?? 0; if ( ! $post_id ) { global $product; if ( $product && is_a( $product, 'WC_Product' ) ) { $post_id = $product->get_id(); } } if ( ! $post_id ) { global $post; if ( $post && get_post_type( $post->ID ) === 'product' ) { $post_id = $post->ID; } } $product = $post_id ? wc_get_product( $post_id ) : null; return $product ? $product : null; } } email-editor/src/Integrations/WooCommerce/class-initializer.php 0000777 00000006761 15252010675 0020756 0 ustar 00 <?php /** * This file is part of the WooCommerce Email Editor package * * @package Automattic\WooCommerce\EmailEditor */ declare( strict_types = 1 ); namespace Automattic\WooCommerce\EmailEditor\Integrations\WooCommerce; use Automattic\WooCommerce\EmailEditor\Engine\Renderer\ContentRenderer\Rendering_Context; use Automattic\WooCommerce\EmailEditor\Integrations\Core\Renderer\Blocks\Abstract_Block_Renderer; use Automattic\WooCommerce\EmailEditor\Integrations\Core\Renderer\Blocks\Fallback; use Automattic\WooCommerce\EmailEditor\Integrations\WooCommerce\Renderer\Blocks\Product_Button; use Automattic\WooCommerce\EmailEditor\Integrations\WooCommerce\Renderer\Blocks\Product_Collection; use Automattic\WooCommerce\EmailEditor\Integrations\WooCommerce\Renderer\Blocks\Product_Image; use Automattic\WooCommerce\EmailEditor\Integrations\WooCommerce\Renderer\Blocks\Product_Price; use Automattic\WooCommerce\EmailEditor\Integrations\WooCommerce\Renderer\Blocks\Product_Sale_Badge; /** * Initializes the WooCommerce blocks renderers. */ class Initializer { /** * List of supported WooCommerce blocks in the email editor. */ const ALLOWED_BLOCK_TYPES = array( 'woocommerce/product-collection', 'woocommerce/product-image', 'woocommerce/product-price', 'woocommerce/product-button', 'woocommerce/product-sale-badge', 'woocommerce/coupon-code', ); /** * Cache renderers by block name. * * @var array<string, Abstract_Block_Renderer> */ private array $renderers = array(); /** * Set `supports.email = true` and configure render_email_callback for supported blocks. * * @param array $settings Block settings. * @return array */ public function update_block_settings( array $settings ): array { if ( in_array( $settings['name'], self::ALLOWED_BLOCK_TYPES, true ) ) { $settings['supports']['email'] = true; $settings['render_email_callback'] = array( $this, 'render_block' ); } return $settings; } /** * Returns the block content rendered by the block renderer. * * @param string $block_content Block content. * @param array $parsed_block Parsed block settings. * @param Rendering_Context $rendering_context Rendering context. * @return string */ public function render_block( string $block_content, array $parsed_block, Rendering_Context $rendering_context ): string { if ( isset( $parsed_block['blockName'] ) ) { $block_renderer = $this->get_block_renderer( $parsed_block['blockName'] ); return $block_renderer->render( $block_content, $parsed_block, $rendering_context ); } return $block_content; } /** * Return an instance of Abstract_Block_Renderer by the block name. * * @param string $block_name Block name. * @return Abstract_Block_Renderer */ private function get_block_renderer( string $block_name ): Abstract_Block_Renderer { if ( isset( $this->renderers[ $block_name ] ) ) { return $this->renderers[ $block_name ]; } switch ( $block_name ) { case 'woocommerce/product-image': $renderer = new Product_Image(); break; case 'woocommerce/product-price': $renderer = new Product_Price(); break; case 'woocommerce/product-sale-badge': $renderer = new Product_Sale_Badge(); break; case 'woocommerce/product-collection': $renderer = new Product_Collection(); break; case 'woocommerce/product-button': $renderer = new Product_Button(); break; default: $renderer = new Fallback(); break; } $this->renderers[ $block_name ] = $renderer; return $renderer; } } email-editor/src/Integrations/Core/class-initializer.php 0000777 00000020021 15252010675 0017410 0 ustar 00 <?php /** * This file is part of the WooCommerce Email Editor package * * @package Automattic\WooCommerce\EmailEditor */ declare( strict_types = 1 ); namespace Automattic\WooCommerce\EmailEditor\Integrations\Core; use Automattic\WooCommerce\EmailEditor\Engine\Renderer\ContentRenderer\Layout\Flex_Layout_Renderer; use Automattic\WooCommerce\EmailEditor\Engine\Renderer\ContentRenderer\Rendering_Context; use Automattic\WooCommerce\EmailEditor\Integrations\Core\Renderer\Blocks\Abstract_Block_Renderer; use Automattic\WooCommerce\EmailEditor\Integrations\Core\Renderer\Blocks\Audio; use Automattic\WooCommerce\EmailEditor\Integrations\Core\Renderer\Blocks\Button; use Automattic\WooCommerce\EmailEditor\Integrations\Core\Renderer\Blocks\Buttons; use Automattic\WooCommerce\EmailEditor\Integrations\Core\Renderer\Blocks\Column; use Automattic\WooCommerce\EmailEditor\Integrations\Core\Renderer\Blocks\Columns; use Automattic\WooCommerce\EmailEditor\Integrations\Core\Renderer\Blocks\Cover; use Automattic\WooCommerce\EmailEditor\Integrations\Core\Renderer\Blocks\Embed; use Automattic\WooCommerce\EmailEditor\Integrations\Core\Renderer\Blocks\Fallback; use Automattic\WooCommerce\EmailEditor\Integrations\Core\Renderer\Blocks\Gallery; use Automattic\WooCommerce\EmailEditor\Integrations\Core\Renderer\Blocks\Group; use Automattic\WooCommerce\EmailEditor\Integrations\Core\Renderer\Blocks\Image; use Automattic\WooCommerce\EmailEditor\Integrations\Core\Renderer\Blocks\List_Block; use Automattic\WooCommerce\EmailEditor\Integrations\Core\Renderer\Blocks\List_Item; use Automattic\WooCommerce\EmailEditor\Integrations\Core\Renderer\Blocks\Media_Text; use Automattic\WooCommerce\EmailEditor\Integrations\Core\Renderer\Blocks\Post_Content; use Automattic\WooCommerce\EmailEditor\Integrations\Core\Renderer\Blocks\Quote; use Automattic\WooCommerce\EmailEditor\Integrations\Core\Renderer\Blocks\Video; use Automattic\WooCommerce\EmailEditor\Integrations\Core\Renderer\Blocks\Social_Link; use Automattic\WooCommerce\EmailEditor\Integrations\Core\Renderer\Blocks\Social_Links; use Automattic\WooCommerce\EmailEditor\Integrations\Core\Renderer\Blocks\Table; use Automattic\WooCommerce\EmailEditor\Integrations\Core\Renderer\Blocks\Text; /** * Initializes the core blocks renderers. */ class Initializer { /** * List of supported WordPress core blocks in the email editor. */ const ALLOWED_BLOCK_TYPES = array( 'core/button', 'core/buttons', 'core/column', 'core/columns', 'core/group', 'core/heading', 'core/image', 'core/list', 'core/list-item', 'core/paragraph', 'core/quote', 'core/spacer', 'core/social-link', 'core/social-links', 'core/site-logo', 'core/site-title', 'core/table', ); /** * List of blocks that only need rendering capabilities (not available in email editor). * * To add a new render-only block: * 1. Add the block name to this array * 2. Optionally create a specific renderer in the Renderer/Blocks directory * 3. Add the renderer case in the get_block_renderer method */ const RENDER_ONLY_BLOCK_TYPES = array( 'core/gallery', 'core/media-text', 'core/audio', 'core/embed', 'core/cover', 'core/video', 'core/post-title', ); /** * Cache renderers by block name. * * @var array<string, Abstract_Block_Renderer> */ private array $renderers = array(); /** * Initializes the core blocks renderers. */ public function initialize(): void { add_filter( 'woocommerce_email_editor_theme_json', array( $this, 'adjust_theme_json' ), 10, 1 ); add_filter( 'safe_style_css', array( $this, 'allow_styles' ) ); } /** * Adjusts the editor's theme to add blocks specific settings for core blocks. * * @param \WP_Theme_JSON $editor_theme_json Editor theme JSON. */ public function adjust_theme_json( \WP_Theme_JSON $editor_theme_json ): \WP_Theme_JSON { $theme_json = (string) file_get_contents( __DIR__ . '/theme.json' ); $theme_json = json_decode( $theme_json, true ); /** * Loaded theme json. * * @var array $theme_json */ $editor_theme_json->merge( new \WP_Theme_JSON( $theme_json, 'default' ) ); return $editor_theme_json; } /** * Allow styles for the email editor. * * @param array|null $allowed_styles Allowed styles. */ public function allow_styles( ?array $allowed_styles ): array { // The styles can be null in some cases. if ( ! is_array( $allowed_styles ) ) { $allowed_styles = array(); } $allowed_styles[] = 'display'; $allowed_styles[] = 'mso-padding-alt'; $allowed_styles[] = 'mso-font-width'; $allowed_styles[] = 'mso-text-raise'; return $allowed_styles; } /** * Configure block settings for email editor support and rendering. * * This method handles three types of blocks: * 1. Editor-available blocks: Set supports.email = true and render_email_callback * 2. Render-only blocks: Only set render_email_callback (not available in editor) * 3. Special blocks: Custom handling (e.g., core/post-content stateless renderer) * * @param array $settings Block settings. * @return array Modified block settings. */ public function update_block_settings( array $settings ): array { // Enable blocks in email editor and set rendering callback. if ( in_array( $settings['name'], self::ALLOWED_BLOCK_TYPES, true ) ) { $settings['supports']['email'] = true; $settings['render_email_callback'] = array( $this, 'render_block' ); } // Set rendering callback for render-only blocks (without enabling in editor). if ( in_array( $settings['name'], self::RENDER_ONLY_BLOCK_TYPES, true ) ) { $settings['render_email_callback'] = array( $this, 'render_block' ); } return $settings; } /** * Returns the block content rendered by the block renderer. * * @param string $block_content Block content. * @param array $parsed_block Parsed block settings. * @param Rendering_Context $rendering_context Rendering context. * @return string */ public function render_block( string $block_content, array $parsed_block, Rendering_Context $rendering_context ): string { if ( isset( $parsed_block['blockName'] ) ) { $block_renderer = $this->get_block_renderer( $parsed_block['blockName'] ); return $block_renderer->render( $block_content, $parsed_block, $rendering_context ); } return $block_content; } /** * Return an instance of Abstract_Block_Renderer by the block name. * * @param string $block_name Block name. * @return Abstract_Block_Renderer */ private function get_block_renderer( string $block_name ): Abstract_Block_Renderer { if ( isset( $this->renderers[ $block_name ] ) ) { return $this->renderers[ $block_name ]; } switch ( $block_name ) { case 'core/heading': case 'core/paragraph': case 'core/site-title': case 'core/post-title': $renderer = new Text(); break; case 'core/column': $renderer = new Column(); break; case 'core/columns': $renderer = new Columns(); break; case 'core/list': $renderer = new List_Block(); break; case 'core/list-item': $renderer = new List_Item(); break; case 'core/image': $renderer = new Image(); break; case 'core/button': $renderer = new Button(); break; case 'core/buttons': $renderer = new Buttons( new Flex_Layout_Renderer() ); break; case 'core/group': $renderer = new Group(); break; case 'core/quote': $renderer = new Quote(); break; case 'core/social-link': $renderer = new Social_Link(); break; case 'core/social-links': $renderer = new Social_Links(); break; case 'core/table': $renderer = new Table(); break; case 'core/gallery': $renderer = new Gallery(); break; case 'core/media-text': $renderer = new Media_Text(); break; case 'core/audio': $renderer = new Audio(); break; case 'core/embed': $renderer = new Embed(); break; case 'core/cover': $renderer = new Cover(); break; case 'core/video': $renderer = new Video(); break; default: $renderer = new Fallback(); break; } $this->renderers[ $block_name ] = $renderer; return $renderer; } } email-editor/src/Integrations/Core/Renderer/Blocks/class-column.php 0000777 00000011454 15252010675 0021357 0 ustar 00 <?php /** * This file is part of the WooCommerce Email Editor package * * @package Automattic\WooCommerce\EmailEditor */ declare( strict_types = 1 ); namespace Automattic\WooCommerce\EmailEditor\Integrations\Core\Renderer\Blocks; use Automattic\WooCommerce\EmailEditor\Engine\Renderer\ContentRenderer\Rendering_Context; use Automattic\WooCommerce\EmailEditor\Integrations\Utils\Dom_Document_Helper; use Automattic\WooCommerce\EmailEditor\Integrations\Utils\Styles_Helper; use Automattic\WooCommerce\EmailEditor\Integrations\Utils\Table_Wrapper_Helper; /** * Renders a column block. */ class Column extends Abstract_Block_Renderer { /** * Override this method to disable spacing (block gap) for columns. * Spacing is applied on wrapping columns block. Columns are rendered side by side so no spacer is needed. * * @param string $content Content. * @param array $email_attrs Email attributes. */ protected function add_spacer( $content, $email_attrs ): string { return $content; } /** * Renders the block content. * * @param string $block_content Block content. * @param array $parsed_block Parsed block. * @param Rendering_Context $rendering_context Rendering context. * @return string */ protected function render_content( string $block_content, array $parsed_block, Rendering_Context $rendering_context ): string { return str_replace( '{column_content}', $this->get_inner_content( $block_content ), $this->get_block_wrapper( $block_content, $parsed_block, $rendering_context ) ); } /** * Based on MJML <mj-column> * * @param string $block_content Block content. * @param array $parsed_block Parsed block. * @param Rendering_Context $rendering_context Rendering context. */ private function get_block_wrapper( string $block_content, array $parsed_block, Rendering_Context $rendering_context ): string { $original_wrapper_classname = ( new Dom_Document_Helper( $block_content ) )->get_attribute_value_by_tag_name( 'div', 'class' ) ?? ''; $block_attributes = wp_parse_args( $parsed_block['attrs'] ?? array(), array( 'verticalAlignment' => 'stretch', 'width' => $rendering_context->get_layout_width_without_padding(), 'style' => array(), ) ); // The default column alignment is `stretch to fill` which means that we need to set the background color to the main cell // to create a feeling of a stretched column. This also needs to apply to CSS classnames which can also apply styles. $is_stretched = empty( $block_attributes['verticalAlignment'] ) || 'stretch' === $block_attributes['verticalAlignment']; $padding_styles = Styles_Helper::get_block_styles( $block_attributes, $rendering_context, array( 'padding' ) ); $padding_styles = Styles_Helper::extend_block_styles( $padding_styles, array( 'text-align' => 'left' ) ); $cell_styles = Styles_Helper::get_block_styles( $block_attributes, $rendering_context, array( 'border', 'background', 'background-color', 'color' ) ); $cell_styles = Styles_Helper::extend_block_styles( $cell_styles, array_filter( array( 'background-size' => ! empty( $cell_styles['background-image'] ) && empty( $cell_styles['background-size'] ) ? 'cover' : null, ) ) ); $wrapper_classname = 'block wp-block-column email-block-column'; $content_classname = 'email-block-column-content'; $wrapper_styles = Styles_Helper::extend_block_styles( Styles_Helper::$empty_block_styles, array( 'vertical-align' => $is_stretched ? 'top' : $block_attributes['verticalAlignment'] ), ); $content_styles = Styles_Helper::extend_block_styles( Styles_Helper::$empty_block_styles, array( 'vertical-align' => 'top' ) ); if ( $is_stretched ) { $wrapper_classname .= ' ' . $original_wrapper_classname; $wrapper_styles = Styles_Helper::extend_block_styles( $wrapper_styles, $cell_styles['declarations'] ); } else { $content_classname .= ' ' . $original_wrapper_classname; $content_styles = Styles_Helper::extend_block_styles( $content_styles, $cell_styles['declarations'] ); } // Create the inner table using the helper. $inner_table_attrs = array( 'class' => $content_classname, 'style' => $content_styles['css'], 'width' => '100%', ); $inner_cell_attrs = array( 'align' => 'left', 'style' => $padding_styles['css'], ); $inner_table = Table_Wrapper_Helper::render_table_wrapper( '{column_content}', $inner_table_attrs, $inner_cell_attrs ); // Create the outer td element (since this is meant to be used within a columns structure). $wrapper_cell_attrs = array( 'class' => $wrapper_classname, 'style' => $wrapper_styles['css'], 'width' => Styles_Helper::parse_value( $block_attributes['width'] ), ); return Table_Wrapper_Helper::render_table_cell( $inner_table, $wrapper_cell_attrs ); } } email-editor/src/Integrations/Core/Renderer/Blocks/class-video.php 0000777 00000014657 15252010675 0021200 0 ustar 00 <?php /** * This file is part of the WooCommerce Email Editor package * * @package Automattic\WooCommerce\EmailEditor */ declare( strict_types = 1 ); namespace Automattic\WooCommerce\EmailEditor\Integrations\Core\Renderer\Blocks; use Automattic\WooCommerce\EmailEditor\Engine\Renderer\ContentRenderer\Rendering_Context; use Automattic\WooCommerce\EmailEditor\Integrations\Utils\Dom_Document_Helper; /** * Video block renderer. * This renderer handles core/video blocks by reusing the cover block renderer * to show a thumbnail with a play button overlay. */ class Video extends Cover { /** * Renders the video block content by transforming it into a cover block structure. * Shows the video poster/thumbnail with a play button overlay using the parent cover renderer. * * @param string $block_content Block content. * @param array $parsed_block Parsed block. * @param Rendering_Context $rendering_context Rendering context. * @return string */ protected function render_content( string $block_content, array $parsed_block, Rendering_Context $rendering_context ): string { $block_attrs = $parsed_block['attrs'] ?? array(); // Extract poster URL from video attributes. $poster_url = $this->extract_poster_url( $block_attrs, $block_content ); // If no poster image, return empty content. if ( empty( $poster_url ) ) { return ''; } // Transform video block into cover block structure and delegate to parent. $cover_block = $this->transform_to_cover_block( $parsed_block, $poster_url ); return parent::render_content( $block_content, $cover_block, $rendering_context ); } /** * Extract poster URL from block attributes. * * @param array $block_attrs Block attributes. * @param string $block_content Original block content (unused, kept for consistency). * @return string Poster URL or empty string. */ private function extract_poster_url( array $block_attrs, string $block_content ): string { // Check for poster attribute. if ( ! empty( $block_attrs['poster'] ) ) { return esc_url( $block_attrs['poster'] ); } return ''; } /** * Extract video URL from block content. * * @param string $block_content Block content HTML. * @return string Video URL or empty string. */ private function extract_video_url( string $block_content ): string { // Use Dom_Document_Helper for robust HTML parsing. $dom_helper = new Dom_Document_Helper( $block_content ); // Find the wp-block-embed__wrapper div. $wrapper_element = $dom_helper->find_element( 'div' ); if ( ! $wrapper_element ) { return ''; } // Check if this div has the correct class. $class_attr = $dom_helper->get_attribute_value( $wrapper_element, 'class' ); if ( strpos( $class_attr, 'wp-block-embed__wrapper' ) === false ) { return ''; } // Get the inner HTML content from the wrapper div. $inner_html = $dom_helper->get_element_inner_html( $wrapper_element ); // Look for HTTP/HTTPS URLs in the inner HTML content. if ( preg_match( '/(?<![a-zA-Z0-9.-])https?:\/\/[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}[a-zA-Z0-9\/?=&%-]*(?![a-zA-Z0-9.-])/', $inner_html, $matches ) ) { $url = $matches[0]; // Decode HTML entities and validate URL. $url = html_entity_decode( $url, ENT_QUOTES | ENT_HTML5, 'UTF-8' ); // Validate the URL. if ( filter_var( $url, FILTER_VALIDATE_URL ) && wp_http_validate_url( $url ) ) { return $url; } } return ''; } /** * Transform a video block into a cover block structure. * * @param array $video_block Original video block. * @param string $poster_url Poster URL to use as background. * @return array Cover block structure. */ private function transform_to_cover_block( array $video_block, string $poster_url ): array { $block_attrs = $video_block['attrs'] ?? array(); $block_content = $video_block['innerHTML'] ?? ''; // Extract video URL from block content, fall back to post URL. // Priority: 1) Video URL (if found), 2) Post permalink (fallback). $video_url = $this->extract_video_url( $block_content ); $link_url = ! empty( $video_url ) ? $video_url : $this->get_current_post_url(); return array( 'blockName' => 'core/cover', 'attrs' => array( 'url' => $poster_url, 'minHeight' => '390px', // Custom attribute for video blocks. ), 'innerBlocks' => array( array( 'blockName' => 'core/html', 'attrs' => array(), 'innerBlocks' => array(), 'innerHTML' => $this->create_play_button_html( $link_url ), 'innerContent' => array( $this->create_play_button_html( $link_url ) ), ), ), 'innerHTML' => $block_content, ); } /** * Create the play button HTML with optional link. * * @param string $link_url Optional URL to link to. * @return string Play button HTML. */ private function create_play_button_html( string $link_url = '' ): string { $play_icon_url = $this->get_play_icon_url(); $play_button = sprintf( '<img src="%s" alt="%s" style="width: 48px; height: 48px; display: inline-block;" />', esc_url( $play_icon_url ), // translators: Alt text for video play button icon. esc_attr( __( 'Play', 'woocommerce' ) ) ); // Wrap the play button in a link if URL is provided. if ( ! empty( $link_url ) ) { $play_button = sprintf( '<a href="%s" target="_blank" rel="noopener noreferrer nofollow" style="display: inline-block; text-decoration: none;">%s</a>', esc_url( $link_url ), $play_button ); } return sprintf( '<p style="text-align: center;">%s</p>', $play_button ); } /** * Get the URL for the play button icon. * * @return string Play button icon URL. */ private function get_play_icon_url(): string { $file_name = '/icons/video/play2x.png'; return plugins_url( $file_name, __FILE__ ); } /** * Get the current post permalink with security validation. * * @return string Post permalink or empty string if invalid. */ private function get_current_post_url(): string { global $post; if ( ! $post instanceof \WP_Post ) { return ''; } $permalink = get_permalink( $post->ID ); if ( empty( $permalink ) ) { return ''; } // Validate URL type and format (following audio block pattern). if ( strpos( $permalink, 'https://' ) !== 0 && strpos( $permalink, 'http://' ) !== 0 ) { // Reject non-HTTP protocols for security. return ''; } // For all HTTP(S) URLs, validate with wp_http_validate_url. if ( ! wp_http_validate_url( $permalink ) ) { return ''; } return $permalink; } } email-editor/src/Integrations/Core/Renderer/Blocks/class-fallback.php 0000777 00000003130 15252010675 0021611 0 ustar 00 <?php /** * This file is part of the WooCommerce Email Editor package * * @package Automattic\WooCommerce\EmailEditor */ declare( strict_types = 1 ); namespace Automattic\WooCommerce\EmailEditor\Integrations\Core\Renderer\Blocks; use Automattic\WooCommerce\EmailEditor\Engine\Renderer\ContentRenderer\Rendering_Context; use Automattic\WooCommerce\EmailEditor\Integrations\Utils\Table_Wrapper_Helper; /** * Fallback block renderer. * This renderer is used when no specific renderer is found for a block. * * AbstractBlockRenderer applies some adjustments to the block content, like adding spacers. * By using fallback renderer for all blocks we apply there adjustments to all blocks that don't have any renderer. * * We need to find a better abstraction/architecture for this. */ class Fallback extends Abstract_Block_Renderer { /** * Renders the block content * * @param string $block_content Block content. * @param array $parsed_block Parsed block. * @param Rendering_Context $rendering_context Rendering context. * @return string */ protected function render_content( $block_content, array $parsed_block, Rendering_Context $rendering_context ): string { $block_attrs = $parsed_block['attrs'] ?? array(); $table_attrs = array( 'style' => 'border-collapse: separate;', // Needed because of border radius. 'width' => '100%', ); $align = $block_attrs['textAlign'] ?? $block_attrs['align'] ?? 'left'; $cell_attrs = array( 'align' => $align, ); return Table_Wrapper_Helper::render_table_wrapper( $block_content, $table_attrs, $cell_attrs ); } } email-editor/src/Integrations/Core/Renderer/Blocks/class-media-text.php 0000777 00000015646 15252010675 0022132 0 ustar 00 <?php /** * This file is part of the WooCommerce Email Editor package * * @package Automattic\WooCommerce\EmailEditor */ declare( strict_types = 1 ); namespace Automattic\WooCommerce\EmailEditor\Integrations\Core\Renderer\Blocks; use Automattic\WooCommerce\EmailEditor\Engine\Renderer\ContentRenderer\Rendering_Context; use Automattic\WooCommerce\EmailEditor\Integrations\Utils\Table_Wrapper_Helper; use Automattic\WooCommerce\EmailEditor\Integrations\Utils\Styles_Helper; use Automattic\WooCommerce\EmailEditor\Integrations\Utils\Dom_Document_Helper; /** * Media-text block renderer. * This renderer handles core/media-text blocks with proper email-friendly HTML layout. */ class Media_Text extends Abstract_Block_Renderer { /** * Renders the media-text block content using a direct table-based layout. * * @param string $block_content Block content. * @param array $parsed_block Parsed block. * @param Rendering_Context $rendering_context Rendering context. * @return string */ protected function render_content( string $block_content, array $parsed_block, Rendering_Context $rendering_context ): string { $block_attrs = $parsed_block['attrs'] ?? array(); $inner_blocks = $parsed_block['innerBlocks'] ?? array(); // Extract media content from innerHTML. $media_content = $this->extract_media_from_html( $parsed_block['innerHTML'] ?? $block_content ); // Render all inner blocks content. $text_content = ''; foreach ( $inner_blocks as $block ) { $text_content .= render_block( $block ); } // If we don't have both media and text content, return empty. if ( empty( $media_content ) || empty( $text_content ) ) { return ''; } // Build the email-friendly layout. return $this->build_email_layout( $media_content, $text_content, $block_attrs, $block_content, $rendering_context ); } /** * Extract media content from the HTML block content. * * @param string $block_content Raw block content. * @return string Media HTML content or empty string if not found. */ private function extract_media_from_html( string $block_content ): string { // Extract inner content from figure element (removing figure wrapper for email compatibility). $media_content = ''; if ( preg_match( '/<figure[^>]*class="[^"]*\bwp-block-media-text__media\b[^"]*"[^>]*>(.*?)<\/figure>/s', $block_content, $matches ) ) { $media_content = trim( $matches[1] ); } return $media_content; } /** * Build the email-friendly layout for media-text blocks. * * @param string $media_content Media HTML content. * @param string $text_content Text content. * @param array $block_attrs Block attributes. * @param string $block_content Original block content. * @param Rendering_Context $rendering_context Rendering context. * @return string Rendered HTML. */ private function build_email_layout( string $media_content, string $text_content, array $block_attrs, string $block_content, Rendering_Context $rendering_context ): string { // Get original wrapper classes from block content. $original_wrapper_classname = ( new Dom_Document_Helper( $block_content ) )->get_attribute_value_by_tag_name( 'div', 'class' ) ?? ''; // Get layout attributes. $media_position = $block_attrs['mediaPosition'] ?? 'left'; $vertical_alignment = $this->get_vertical_alignment_from_attributes( $block_attrs ); $media_width = $this->get_media_width_from_attributes( $block_attrs ); $text_width = 100 - $media_width; // Text takes the remaining width. // Handle image linking for any linkDestination type that has an href. if ( ! empty( $block_attrs['href'] ) ) { $media_content = $this->wrap_media_with_link( $media_content, $block_attrs['href'] ); } // Get block styles using the Styles_Helper. $block_styles = Styles_Helper::get_block_styles( $block_attrs, $rendering_context, array( 'padding', 'border', 'background', 'background-color', 'color' ) ); $block_styles = Styles_Helper::extend_block_styles( $block_styles, array( 'width' => '100%', 'border-collapse' => 'collapse', 'text-align' => 'left', ) ); // Apply class and style attributes to the wrapper table. $table_attrs = array( 'class' => 'email-block-media-text ' . $original_wrapper_classname, 'style' => $block_styles['css'], 'align' => 'left', 'width' => '100%', ); // Build individual table cells. $media_cell_attrs = array( 'style' => sprintf( 'width: %d%%; padding: 10px; vertical-align: %s;', $media_width, $vertical_alignment ), 'valign' => $vertical_alignment, ); $text_cell_attrs = array( 'style' => sprintf( 'width: %d%%; padding: 0 8%%; vertical-align: %s;', $text_width, $vertical_alignment ), 'valign' => $vertical_alignment, ); $media_cell = Table_Wrapper_Helper::render_table_cell( $media_content, $media_cell_attrs ); $text_cell = Table_Wrapper_Helper::render_table_cell( $text_content, $text_cell_attrs ); // Order cells based on media position. if ( 'right' === $media_position ) { // Text first, then media. $cells = $text_cell . $media_cell; } else { // Media first, then text (default left position). $cells = $media_cell . $text_cell; } // Use render_cell = false to avoid wrapping in an extra <td>. return Table_Wrapper_Helper::render_table_wrapper( $cells, $table_attrs, array(), array(), false ); } /** * Get the vertical alignment value from block attributes. * * @param array $block_attrs Block attributes. * @return string CSS vertical-align value. */ private function get_vertical_alignment_from_attributes( array $block_attrs ): string { $vertical_alignment = $block_attrs['verticalAlignment'] ?? 'middle'; // Convert WordPress alignment values to CSS values. switch ( $vertical_alignment ) { case 'top': return 'top'; case 'center': return 'middle'; case 'bottom': return 'bottom'; default: return 'middle'; } } /** * Get the media width value from block attributes. * * @param array $block_attrs Block attributes. * @return int Media width percentage (1-99). */ private function get_media_width_from_attributes( array $block_attrs ): int { $media_width = $block_attrs['mediaWidth'] ?? 50; // Ensure the width is within reasonable bounds. $media_width = max( 1, min( 99, (int) $media_width ) ); return $media_width; } /** * Wrap media content with a link if it's not already wrapped. * * @param string $media_content The media content (inner content from figure element). * @param string $href The URL to link to. * @return string Media content wrapped with link. */ private function wrap_media_with_link( string $media_content, string $href ): string { // If media is already wrapped in a link, return as-is. if ( false !== strpos( $media_content, '<a ' ) ) { return $media_content; } // Wrap the media content with a link. return '<a href="' . esc_url( $href ) . '">' . $media_content . '</a>'; } } email-editor/src/Integrations/Core/Renderer/Blocks/class-table.php 0000777 00000054143 15252010675 0021153 0 ustar 00 <?php /** * This file is part of the WooCommerce Email Editor package * * @package Automattic\WooCommerce\EmailEditor */ declare( strict_types = 1 ); namespace Automattic\WooCommerce\EmailEditor\Integrations\Core\Renderer\Blocks; use Automattic\WooCommerce\EmailEditor\Engine\Renderer\ContentRenderer\Rendering_Context; use Automattic\WooCommerce\EmailEditor\Integrations\Utils\Dom_Document_Helper; use Automattic\WooCommerce\EmailEditor\Integrations\Utils\Styles_Helper; use Automattic\WooCommerce\EmailEditor\Integrations\Utils\Table_Wrapper_Helper; use Automattic\WooCommerce\EmailEditor\Integrations\Utils\Html_Processing_Helper; /** * Renders a table block. */ class Table extends Abstract_Block_Renderer { /** * Valid text alignment values. */ private const VALID_TEXT_ALIGNMENTS = array( 'left', 'center', 'right' ); /** * Renders the block content. * * @param string $block_content Block content. * @param array $parsed_block Parsed block. * @param Rendering_Context $rendering_context Rendering context. * @return string */ protected function render_content( string $block_content, array $parsed_block, Rendering_Context $rendering_context ): string { // Extract table content and caption from figure wrapper if present. $extracted_data = $this->extract_table_and_caption_from_figure( $block_content ); $table_content = $extracted_data['table']; $caption = $extracted_data['caption']; // Validate that we have actual table content. if ( ! $this->is_valid_table_content( $table_content ) ) { return ''; } // Check for empty table structures - tables with no th or td elements. if ( ! preg_match( '/<(th|td)/i', $table_content ) ) { return ''; } $block_attributes = wp_parse_args( $parsed_block['attrs'] ?? array(), array( 'textAlign' => 'left', 'style' => array(), ) ); $html = new \WP_HTML_Tag_Processor( $table_content ); $classes = 'email-table-block'; if ( $html->next_tag() ) { $block_classes = (string) ( $html->get_attribute( 'class' ) ?? '' ); $classes .= ' ' . $block_classes; // Clean classes for table element. $block_classes = Html_Processing_Helper::clean_css_classes( $block_classes ); $html->set_attribute( 'class', $block_classes ); $table_content = $html->get_updated_html(); } // Clean wrapper classes. $classes = Html_Processing_Helper::clean_css_classes( $classes ); // Get spacing styles for wrapper and table-specific styles separately. $spacing_styles = Styles_Helper::get_block_styles( $block_attributes, $rendering_context, array( 'spacing' ) ); $table_styles = Styles_Helper::get_block_styles( $block_attributes, $rendering_context, array( 'background-color', 'color', 'typography' ) ); // Ensure background styles are completely removed from spacing styles and force transparent background. $spacing_css = $spacing_styles['css'] ?? ''; $spacing_css = (string) ( preg_replace( '/background[^;]*;?/', '', $spacing_css ) ?? '' ); $spacing_css = (string) ( preg_replace( '/\s*;\s*;/', ';', $spacing_css ) ?? '' ); // Clean up double semicolons. $spacing_css = trim( $spacing_css, '; ' ); // Force transparent background on wrapper to prevent any background leakage. $spacing_styles['css'] = $spacing_css ? $spacing_css . '; background: transparent !important;' : 'background: transparent !important;'; $additional_styles = array( 'min-width' => '100%', // Prevent Gmail App from shrinking the table on mobile devices. ); // Add fallback text color when no custom text color or preset text color is set. if ( empty( $table_styles['declarations']['color'] ) ) { $email_styles = $rendering_context->get_theme_styles(); $color = $parsed_block['email_attrs']['color'] ?? $email_styles['color']['text'] ?? '#000000'; // Sanitize color value to ensure it's a valid hex color. $additional_styles['color'] = Html_Processing_Helper::sanitize_color( $color ); } $additional_styles['text-align'] = 'left'; if ( ! empty( $parsed_block['attrs']['textAlign'] ) ) { // In this case, textAlign needs to be one of 'left', 'center', 'right'. $text_align = $parsed_block['attrs']['textAlign']; if ( in_array( $text_align, self::VALID_TEXT_ALIGNMENTS, true ) ) { $additional_styles['text-align'] = $text_align; } } elseif ( in_array( $parsed_block['attrs']['align'] ?? null, self::VALID_TEXT_ALIGNMENTS, true ) ) { $additional_styles['text-align'] = $parsed_block['attrs']['align']; } $table_styles = Styles_Helper::extend_block_styles( $table_styles, $additional_styles ); // Check if this is a striped table style. $is_striped_table = $this->is_striped_table( $block_content, $parsed_block ); // Process the table content to ensure email compatibility BEFORE wrapping. $table_content = $this->process_table_content( $table_content, $parsed_block, $rendering_context, $is_striped_table ); // Apply table-specific styles (background, color, typography) directly to the table element. $table_content_with_styles = $this->apply_styles_to_table_element( $table_content, $table_styles['css'] ); // Add wp-block-table class to the table element for theme.json CSS rules. if ( false !== strpos( $block_content, 'wp-block-table' ) ) { $table_content_with_styles = $this->add_class_to_table_element( $table_content_with_styles, 'wp-block-table' ); } // Build complete content (table + caption). $complete_content = $table_content_with_styles; if ( ! empty( $caption ) ) { // Use HTML API to safely allow specific tags in caption. $sanitized_caption = Html_Processing_Helper::sanitize_caption_html( $caption ); // Extract typography styles from table styles (not spacing styles) and apply to caption. $caption_styles = $this->extract_typography_styles_for_caption( $table_styles['css'] ); $complete_content .= '<div style="text-align: center; margin-top: 8px; ' . $caption_styles . '">' . $sanitized_caption . '</div>'; } $table_attrs = array( 'style' => 'border-collapse: separate;', // Needed because of border radius. 'width' => '100%', ); // Use spacing styles only for the wrapper. $cell_attrs = array( 'class' => $classes, 'style' => $spacing_styles['css'], 'align' => $additional_styles['text-align'], ); $rendered_table = Table_Wrapper_Helper::render_table_wrapper( $complete_content, $table_attrs, $cell_attrs ); return $rendered_table; } /** * Process table content to ensure email client compatibility. * * @param string $block_content Block content. * @param array $parsed_block Parsed block. * @param Rendering_Context $rendering_context Rendering context. * @param bool $is_striped_table Whether this is a striped table. * @return string */ private function process_table_content( string $block_content, array $parsed_block, Rendering_Context $rendering_context, bool $is_striped_table = false ): string { $html = new \WP_HTML_Tag_Processor( $block_content ); // Extract custom border color and width from block attributes. $custom_border_color = $this->get_custom_border_color( $parsed_block, $rendering_context ); $custom_border_width = $this->get_custom_border_width( $parsed_block ); // Use custom border color if available, otherwise fall back to default. if ( $custom_border_color ) { $border_color = $custom_border_color; } else { // Get theme styles once to avoid repeated calls. $email_styles = $rendering_context->get_theme_styles(); $border_color = Html_Processing_Helper::sanitize_color( $parsed_block['email_attrs']['color'] ?? $email_styles['color']['text'] ?? '#000000' ); } // Track row context for striped styling. $current_section = ''; // Table sections: thead, tbody, tfoot. $row_count = 0; // Process table elements. while ( $html->next_tag() ) { $tag_name = $html->get_tag(); if ( 'TABLE' === $tag_name ) { // Ensure table has proper email attributes. $html->set_attribute( 'border', '1' ); $html->set_attribute( 'cellpadding', '8' ); $html->set_attribute( 'cellspacing', '0' ); $html->set_attribute( 'role', 'presentation' ); $html->set_attribute( 'width', '100%' ); // Get existing style and add email-specific styles. $existing_style = (string) ( $html->get_attribute( 'style' ) ?? '' ); // Check for fixed layout class and apply table-layout: fixed. $class_attr = (string) ( $html->get_attribute( 'class' ) ?? '' ); $table_layout = $this->has_fixed_layout( $class_attr ) ? 'table-layout: fixed; ' : ''; // Use border-collapse: collapse to ensure consistent borders between table and cells. $email_table_styles = "{$table_layout}border-collapse: collapse; width: 100%;"; $existing_style = rtrim( $existing_style, "; \t\n\r\0\x0B" ); $new_style = $existing_style ? $existing_style . '; ' . $email_table_styles : $email_table_styles; $html->set_attribute( 'style', $new_style ); // Remove problematic classes from the table but keep has-fixed-layout and alignment classes for editor UI. $class_attr = Html_Processing_Helper::clean_css_classes( $class_attr ); $html->set_attribute( 'class', $class_attr ); } elseif ( 'THEAD' === $tag_name ) { $current_section = 'thead'; $row_count = 0; } elseif ( 'TBODY' === $tag_name ) { $current_section = 'tbody'; $row_count = 0; } elseif ( 'TFOOT' === $tag_name ) { $current_section = 'tfoot'; $row_count = 0; } elseif ( 'TR' === $tag_name ) { ++$row_count; } elseif ( 'TD' === $tag_name || 'TH' === $tag_name ) { // Ensure table cells have proper email attributes with borders and padding. $html->set_attribute( 'valign', 'top' ); // Get existing style and add email-specific styles with borders and padding. $existing_style = (string) ( $html->get_attribute( 'style' ) ?? '' ); $existing_style = rtrim( $existing_style, "; \t\n\r\0\x0B" ); $border_width = $custom_border_width ? $custom_border_width : '1px'; $border_style = $this->get_custom_border_style( $parsed_block ); // Extract cell-specific text alignment. $cell_text_align = $this->get_cell_text_alignment( $html ); $email_cell_styles = "vertical-align: top; border: {$border_width} {$border_style} {$border_color}; padding: 8px; text-align: {$cell_text_align};"; // Add thicker borders for header and footer cells when no custom border is set. $email_cell_styles = $this->add_header_footer_borders( $html, $email_cell_styles, $border_color, $current_section, $custom_border_width ); // Add striped styling for tbody rows (first row gets background, then alternates). if ( $is_striped_table && 'tbody' === $current_section && 1 === $row_count % 2 ) { $email_cell_styles .= ' background-color: #f8f9fa;'; } $new_cell_style = $existing_style ? $existing_style . '; ' . $email_cell_styles : $email_cell_styles; $html->set_attribute( 'style', $new_cell_style ); } } return $html->get_updated_html(); } /** * Get custom border color from block attributes. * * @param array $parsed_block Parsed block. * @param Rendering_Context $rendering_context Rendering context. * @return string|null Custom border color or null if not set. */ private function get_custom_border_color( array $parsed_block, Rendering_Context $rendering_context ): ?string { $block_attributes = $parsed_block['attrs'] ?? array(); if ( ! empty( $block_attributes['borderColor'] ) ) { $border_color = $rendering_context->translate_slug_to_color( $block_attributes['borderColor'] ); return Html_Processing_Helper::sanitize_color( $border_color ); } return null; } /** * Get custom border width from block attributes. * * @param array $parsed_block Parsed block. * @return string|null Custom border width or null if not set. */ private function get_custom_border_width( array $parsed_block ): ?string { $block_attributes = $parsed_block['attrs'] ?? array(); if ( ! empty( $block_attributes['style']['border']['width'] ) ) { $border_width = $block_attributes['style']['border']['width']; // Sanitize the border width value. $border_width = Html_Processing_Helper::sanitize_css_value( $border_width ); if ( empty( $border_width ) ) { return null; } // Ensure the border width has a unit, default to px if not specified. if ( is_numeric( $border_width ) ) { return $border_width . 'px'; } // Validate that the border width contains only valid CSS units and numbers. if ( preg_match( '/^[0-9]+\.?[0-9]*(px|em|rem|pt|pc|in|cm|mm|ex|ch|vw|vh|vmin|vmax)$/', $border_width ) ) { return $border_width; } // If invalid, return null to use default. return null; } return null; } /** * Get custom border style from block attributes. * * @param array $parsed_block Parsed block. * @return string Custom border style or 'solid' as default. */ private function get_custom_border_style( array $parsed_block ): string { $style = strtolower( (string) ( $parsed_block['attrs']['style']['border']['style'] ?? '' ) ); $allowed = array( 'solid', 'dashed', 'dotted' ); // Email-safe subset. return in_array( $style, $allowed, true ) ? $style : 'solid'; } /** * Add thicker borders for table headers and footers when no custom border is set. * * @param \WP_HTML_Tag_Processor $html HTML tag processor. * @param string $base_styles Base cell styles. * @param string $border_color Border color. * @param string $current_section Current table section (thead, tbody, tfoot). * @param string|null $custom_border_width Custom border width if set. * @return string Updated cell styles. */ private function add_header_footer_borders( \WP_HTML_Tag_Processor $html, string $base_styles, string $border_color, string $current_section = '', ?string $custom_border_width = null ): string { $tag_name = $html->get_tag(); // Only add thicker borders if no custom border width is set. if ( $custom_border_width ) { return $base_styles; } // Add thicker bottom border to all TH elements (headers). if ( 'TH' === $tag_name ) { $base_styles .= " border-bottom: 3px solid {$border_color};"; } // Add thicker top border to footer cells (TD elements in tfoot). if ( 'TD' === $tag_name && 'tfoot' === $current_section ) { $base_styles .= " border-top: 3px solid {$border_color};"; } return $base_styles; } /** * Get text alignment for a table cell. * * @param \WP_HTML_Tag_Processor $html HTML tag processor. * @return string Text alignment value (left, center, right). */ private function get_cell_text_alignment( \WP_HTML_Tag_Processor $html ): string { // Check for data-align attribute first. $data_align = $html->get_attribute( 'data-align' ); if ( $data_align && in_array( $data_align, self::VALID_TEXT_ALIGNMENTS, true ) ) { return $data_align; } // Check for has-text-align-* classes. $class_attr = (string) ( $html->get_attribute( 'class' ) ?? '' ); if ( false !== strpos( $class_attr, 'has-text-align-center' ) ) { return 'center'; } if ( false !== strpos( $class_attr, 'has-text-align-right' ) ) { return 'right'; } if ( false !== strpos( $class_attr, 'has-text-align-left' ) ) { return 'left'; } // Default to left alignment. return 'left'; } /** * Check if table has fixed layout class. * * @param string $class_attr Class attribute string. * @return bool True if has-fixed-layout class is present. */ private function has_fixed_layout( string $class_attr ): bool { return false !== strpos( $class_attr, 'has-fixed-layout' ); } /** * Extract table content and caption from figure wrapper if present. * * @param string $block_content Block content. * @return array Array with 'table' and 'caption' keys. */ private function extract_table_and_caption_from_figure( string $block_content ): array { $dom_helper = new Dom_Document_Helper( $block_content ); // Look for figure element with wp-block-table class. $figure_tag = $dom_helper->find_element( 'figure' ); if ( ! $figure_tag ) { // If no figure wrapper found, return original content as table. return array( 'table' => $block_content, 'caption' => '', ); } $figure_class_attr = $dom_helper->get_attribute_value( $figure_tag, 'class' ); $figure_class = (string) ( $figure_class_attr ? $figure_class_attr : '' ); if ( false === strpos( $figure_class, 'wp-block-table' ) ) { // If figure doesn't have wp-block-table class, return original content as table. return array( 'table' => $block_content, 'caption' => '', ); } // Extract table element from within the matched figure only. $figure_html = $dom_helper->get_outer_html( $figure_tag ); // Use regex to extract table from within the figure to avoid document conflicts. if ( ! preg_match( '/<table[^>]*>.*?<\/table>/is', $figure_html, $table_matches ) ) { return array( 'table' => $block_content, 'caption' => '', ); } $table_html = $table_matches[0]; // Extract figcaption if present (scoped to the figure). $caption = ''; if ( preg_match( '/<figcaption[^>]*>(.*?)<\/figcaption>/is', $figure_html, $figcaption_matches ) ) { $caption = $figcaption_matches[1]; } return array( 'table' => $table_html, 'caption' => $caption, ); } /** * Apply CSS styles directly to the table element. * * @param string $table_content Table HTML content. * @param string $styles CSS styles to apply. * @return string Table content with styles applied. */ private function apply_styles_to_table_element( string $table_content, string $styles ): string { $html = new \WP_HTML_Tag_Processor( $table_content ); if ( $html->next_tag( array( 'tag_name' => 'TABLE' ) ) ) { $existing_style = (string) ( $html->get_attribute( 'style' ) ?? '' ); $existing_style = rtrim( $existing_style, "; \t\n\r\0\x0B" ); // Add default border widths if individual border colors are present but no widths. $border_width_styles = $this->get_default_border_widths( $existing_style ); $new_style = $existing_style; if ( ! empty( $border_width_styles ) ) { $new_style = $new_style ? $new_style . '; ' . $border_width_styles : $border_width_styles; } if ( ! empty( $styles ) ) { $new_style = $new_style ? $new_style . '; ' . $styles : $styles; } $html->set_attribute( 'style', $new_style ); return $html->get_updated_html(); } return $table_content; } /** * Get default border widths for table element when individual border colors are present. * * @param string $existing_style Existing style attribute of the table element. * @return string CSS border width styles or empty string if not needed. */ private function get_default_border_widths( string $existing_style ): string { // Check if individual border colors are present but no corresponding widths. $sides = array( 'top', 'right', 'bottom', 'left' ); $border_width_styles = array(); foreach ( $sides as $side ) { $has_color = strpos( $existing_style, "border-{$side}-color:" ) !== false; $has_width = strpos( $existing_style, "border-{$side}-width:" ) !== false; // If border color is present but no width, add default width. if ( $has_color && ! $has_width ) { $border_width_styles[] = "border-{$side}-width: 1.5px"; } } return implode( '; ', $border_width_styles ); } /** * Add a CSS class to the table element. * * @param string $table_content Table HTML content. * @param string $class_name CSS class to add. * @return string Table content with class added. */ private function add_class_to_table_element( string $table_content, string $class_name ): string { // Validate class name to prevent XSS. if ( ! preg_match( '/^[a-zA-Z0-9\-_]+$/', $class_name ) ) { return $table_content; } $html = new \WP_HTML_Tag_Processor( $table_content ); if ( $html->next_tag( array( 'tag_name' => 'TABLE' ) ) ) { $existing_class = (string) ( $html->get_attribute( 'class' ) ?? '' ); $existing_class = trim( $existing_class ); // Only add if not already present. if ( false === strpos( $existing_class, $class_name ) ) { $new_class = $existing_class ? $existing_class . ' ' . $class_name : $class_name; $html->set_attribute( 'class', $new_class ); } return $html->get_updated_html(); } return $table_content; } /** * Extract typography styles from CSS string for caption. * * @param string $css CSS string to extract typography from. * @return string Typography CSS for caption. */ private function extract_typography_styles_for_caption( string $css ): string { $typography_properties = Html_Processing_Helper::get_caption_css_properties(); $caption_styles = array(); foreach ( $typography_properties as $property ) { // Use regex to extract each typography property. if ( preg_match( '/' . preg_quote( $property, '/' ) . '\s*:\s*([^;]+)/i', $css, $matches ) ) { $value = trim( $matches[1] ); // Sanitize the CSS value to prevent injection. $sanitized_value = Html_Processing_Helper::sanitize_css_value( $value ); if ( ! empty( $sanitized_value ) ) { $caption_styles[] = $property . ': ' . $sanitized_value; } } } return implode( '; ', $caption_styles ); } /** * Check if the table has striped styling. * * @param string $block_content Block content. * @param array $parsed_block Parsed block. * @return bool True if it's a striped table, false otherwise. */ private function is_striped_table( string $block_content, array $parsed_block ): bool { // Check for is-style-stripes in block attributes. if ( isset( $parsed_block['attrs']['className'] ) && false !== strpos( $parsed_block['attrs']['className'], 'is-style-stripes' ) ) { return true; } // Check for is-style-stripes in figure classes. if ( false !== strpos( $block_content, 'is-style-stripes' ) ) { return true; } return false; } /** * Validate if the content is a valid table HTML. * * @param string $content The content to validate. * @return bool True if it's a valid table, false otherwise. */ private function is_valid_table_content( string $content ): bool { // Only assert that a <table> exists; downstream checks handle emptiness and KSES handles sanitization. return (bool) preg_match( '/<table[^>]*>.*?<\/table>/is', $content ); } } email-editor/src/Integrations/Core/Renderer/Blocks/class-gallery.php 0000777 00000026643 15252010675 0021527 0 ustar 00 <?php /** * This file is part of the WooCommerce Email Editor package * * @package Automattic\WooCommerce\EmailEditor */ declare( strict_types = 1 ); namespace Automattic\WooCommerce\EmailEditor\Integrations\Core\Renderer\Blocks; use Automattic\WooCommerce\EmailEditor\Engine\Renderer\ContentRenderer\Rendering_Context; use Automattic\WooCommerce\EmailEditor\Integrations\Utils\Table_Wrapper_Helper; use Automattic\WooCommerce\EmailEditor\Integrations\Utils\Styles_Helper; use Automattic\WooCommerce\EmailEditor\Integrations\Utils\Dom_Document_Helper; use Automattic\WooCommerce\EmailEditor\Integrations\Utils\Html_Processing_Helper; /** * Gallery block renderer. * This renderer handles core/gallery blocks with proper email-friendly HTML layout. */ class Gallery extends Abstract_Block_Renderer { /** * Renders the gallery block content using a table-based layout. * * @param string $block_content Block content. * @param array $parsed_block Parsed block. * @param Rendering_Context $rendering_context Rendering context. * @return string */ protected function render_content( string $block_content, array $parsed_block, Rendering_Context $rendering_context ): string { // Extract images directly from the block content (more efficient than re-rendering). $gallery_images = $this->extract_images_from_gallery_content( $block_content, $parsed_block ); // If we don't have any images, return empty. if ( empty( $gallery_images ) ) { return ''; } // Build the email-friendly layout. return $this->build_email_layout( $gallery_images, $parsed_block, $block_content, $rendering_context ); } /** * Extract all images from gallery content with their links and captions. * * @param string $block_content The rendered gallery block HTML. * @param array $parsed_block The parsed block data. * @return array Array of sanitized image HTML strings. */ private function extract_images_from_gallery_content( string $block_content, array $parsed_block ): array { $gallery_images = array(); $inner_blocks = $parsed_block['innerBlocks'] ?? array(); // Extract images from inner blocks data where the actual image HTML is stored. foreach ( $inner_blocks as $block ) { if ( 'core/image' === $block['blockName'] && isset( $block['innerHTML'] ) ) { $extracted_image = $this->extract_image_from_html( $block['innerHTML'] ); if ( ! empty( $extracted_image ) ) { $gallery_images[] = $extracted_image; } } } return $gallery_images; } /** * Extract and sanitize image with optional link and caption from HTML content. * This is the unified method that handles all image extraction scenarios. * * @param string $html_content HTML content containing the image. * @return string Sanitized image HTML with proper link and caption handling. */ private function extract_image_from_html( string $html_content ): string { $result = ''; // First, try to find a linked image (most common case). if ( preg_match( '/<a[^>]*href=(["\'])(.*?)\1[^>]*>(\s*<img[^>]*>)\s*<\/a>/s', $html_content, $link_matches ) ) { // Validate and sanitize the link URL. $sanitized_url = esc_url( $link_matches[2] ); if ( ! empty( $sanitized_url ) ) { $sanitized_img = Html_Processing_Helper::sanitize_image_html( $link_matches[3] ); if ( '' !== $sanitized_img ) { $result .= '<a href="' . $sanitized_url . '">' . $sanitized_img . '</a>'; } } else { // If URL is invalid, extract just the image without link. $sanitized_img = Html_Processing_Helper::sanitize_image_html( $link_matches[3] ); if ( '' !== $sanitized_img ) { $result .= $sanitized_img; } } } elseif ( preg_match( '/<img[^>]*>/', $html_content, $img_matches ) ) { // Image is not linked - just extract the img element with sanitization. $sanitized_img = Html_Processing_Helper::sanitize_image_html( $img_matches[0] ); if ( '' !== $sanitized_img ) { $result .= $sanitized_img; } } // Extract the caption if it exists (handle both figcaption and span formats). // Enhanced security: validate container attributes before extracting content. if ( preg_match( '/(<figcaption[^>]*>)(.*?)(<\/figcaption>)/s', $html_content, $caption_matches ) ) { // Validate the figcaption container attributes for security. if ( Html_Processing_Helper::validate_container_attributes( $caption_matches[1] . $caption_matches[3] ) ) { $sanitized_caption = Html_Processing_Helper::sanitize_caption_html( $caption_matches[2] ); $result .= '<br><div class="wp-element-caption" style="font-size: 13px; line-height: 1.0;">' . $sanitized_caption . '</div>'; } } elseif ( preg_match( '/(<span class="wp-element-caption"[^>]*>)(.*?)(<\/span>)/s', $html_content, $caption_matches ) ) { // Validate the span container attributes for security. if ( Html_Processing_Helper::validate_container_attributes( $caption_matches[1] . $caption_matches[3] ) ) { $sanitized_caption = Html_Processing_Helper::sanitize_caption_html( $caption_matches[2] ); $result .= '<br><div class="wp-element-caption" style="font-size: 13px; line-height: 1.0;">' . $sanitized_caption . '</div>'; } } return $result; } /** * Extract gallery-level caption from the original block content. * * @param string $block_content Original block content. * @return string Gallery caption or empty string if not found. */ private function extract_gallery_caption( string $block_content ): string { // Look for gallery-level caption: <figcaption class="blocks-gallery-caption wp-element-caption">. // Enhanced security: validate container attributes before extracting content. if ( preg_match( '/(<figcaption class="blocks-gallery-caption[^"]*"[^>]*>)(.*?)(<\/figcaption>)/s', $block_content, $matches ) ) { // Validate the figcaption container attributes for security. if ( Html_Processing_Helper::validate_container_attributes( $matches[1] . $matches[3] ) ) { return Html_Processing_Helper::sanitize_caption_html( trim( $matches[2] ) ); } } return ''; } /** * Build the email-friendly layout for gallery blocks. * * @param array $gallery_images Array of image HTML strings. * @param array $parsed_block Full parsed block data. * @param string $block_content Original block content. * @param Rendering_Context $rendering_context Rendering context. * @return string Rendered HTML. */ private function build_email_layout( array $gallery_images, array $parsed_block, string $block_content, Rendering_Context $rendering_context ): string { // Get original wrapper classes from block content. $original_wrapper_classname = ( new Dom_Document_Helper( $block_content ) )->get_attribute_value_by_tag_name( 'figure', 'class' ) ?? ''; // Get gallery attributes. $block_attrs = $parsed_block['attrs'] ?? array(); $columns = $this->get_columns_from_attributes( $block_attrs ); // Extract gallery-level caption from the original block content. $gallery_caption = $this->extract_gallery_caption( $block_content ); // Get block styles using the Styles_Helper. $block_styles = Styles_Helper::get_block_styles( $block_attrs, $rendering_context, array( 'padding', 'border', 'background', 'background-color', 'color' ) ); $block_styles = Styles_Helper::extend_block_styles( $block_styles, array( 'width' => '100%', 'border-collapse' => 'collapse', 'text-align' => 'left', ) ); // Apply class and style attributes to the wrapper table. $table_attrs = array( 'class' => 'email-block-gallery ' . Html_Processing_Helper::clean_css_classes( $original_wrapper_classname ), 'style' => $block_styles['css'], 'align' => 'left', 'width' => '100%', ); // Add email width to cell attributes if available. $cell_attrs = array(); if ( isset( $parsed_block['email_attrs']['width'] ) ) { $cell_attrs['width'] = $parsed_block['email_attrs']['width']; } // Build the gallery rows with proper table structure. $gallery_content = $this->build_gallery_table( $gallery_images, $columns ); // Add gallery caption if it exists. if ( ! empty( $gallery_caption ) ) { $gallery_content .= '<br><div class="blocks-gallery-caption wp-element-caption" style="font-size: 13px; line-height: 1.0; text-align: center;">' . $gallery_caption . '</div>'; } // Use Table_Wrapper_Helper for the main container (following tiled gallery pattern). return Table_Wrapper_Helper::render_table_wrapper( $gallery_content, $table_attrs, $cell_attrs ); } /** * Build the gallery table structure with proper rows and cells. * Uses the tiled gallery pattern: separate tables for each row, then wrap in main table. * * @param array $gallery_images Array of image HTML strings. * @param int $columns Number of columns. * @return string Gallery table HTML. */ private function build_gallery_table( array $gallery_images, int $columns ): string { $content_parts = array(); $image_count = count( $gallery_images ); $cell_padding = 8; // 0.5em equivalent (approximately 8px) // Process images in chunks based on columns to create rows. for ( $i = 0; $i < $image_count; $i += $columns ) { $row_images = array_slice( $gallery_images, $i, $columns ); $content_parts[] = $this->build_gallery_row_table( $row_images, $columns, $cell_padding ); } return implode( '', $content_parts ); } /** * Build a single gallery row as a separate table (following tiled gallery pattern). * * @param array $row_images Images for this row. * @param int $total_columns Total number of columns. * @param int $cell_padding Cell padding. * @return string Row table HTML. */ private function build_gallery_row_table( array $row_images, int $total_columns, int $cell_padding ): string { $images_in_row = count( $row_images ); $row_cells = ''; // If there is exactly one image, span full width; otherwise distribute width evenly across the images in this row. if ( 1 === $images_in_row ) { $cell_attrs = array( 'style' => sprintf( 'width: %s; padding: %dpx; vertical-align: top; text-align: center;', Html_Processing_Helper::sanitize_css_value( '100%' ), $cell_padding ), 'valign' => 'top', 'colspan' => $total_columns, ); $row_cells .= Table_Wrapper_Helper::render_table_cell( $row_images[0], $cell_attrs ); } else { // Evenly distribute available width among the images in this row. $cell_width_percent = 100 / $images_in_row; foreach ( $row_images as $image_html ) { $cell_attrs = array( 'style' => sprintf( 'width: %s; padding: %dpx; vertical-align: top; text-align: center;', Html_Processing_Helper::sanitize_css_value( sprintf( '%.2f%%', $cell_width_percent ) ), $cell_padding ), 'valign' => 'top', ); $row_cells .= Table_Wrapper_Helper::render_table_cell( $image_html, $cell_attrs ); } } // Create a separate table for this row (following tiled gallery pattern). return sprintf( '<table role="presentation" style="width: %s; border-collapse: collapse; table-layout: fixed;"><tr>%s</tr></table>', Html_Processing_Helper::sanitize_css_value( '100%' ), $row_cells ); } /** * Get the columns value from block attributes. * * @param array $block_attrs Block attributes. * @return int Number of columns (1-5). */ private function get_columns_from_attributes( array $block_attrs ): int { $columns = $block_attrs['columns'] ?? 3; // Ensure the columns are within reasonable bounds. $columns = max( 1, min( 5, (int) $columns ) ); return $columns; } } email-editor/src/Integrations/Core/Renderer/Blocks/class-list-item.php 0000777 00000002063 15252010675 0021765 0 ustar 00 <?php /** * This file is part of the WooCommerce Email Editor package * * @package Automattic\WooCommerce\EmailEditor */ declare( strict_types = 1 ); namespace Automattic\WooCommerce\EmailEditor\Integrations\Core\Renderer\Blocks; use Automattic\WooCommerce\EmailEditor\Engine\Renderer\ContentRenderer\Rendering_Context; /** * Renders a list item block. */ class List_Item extends Abstract_Block_Renderer { /** * Override this method to disable spacing (block gap) for list items. * * @param string $content Content. * @param array $email_attrs Email attributes. */ protected function add_spacer( $content, $email_attrs ): string { return $content; } /** * Renders the block content. * * @param string $block_content Block content. * @param array $parsed_block Parsed block. * @param Rendering_Context $rendering_context Rendering context. * @return string */ protected function render_content( $block_content, array $parsed_block, Rendering_Context $rendering_context ): string { return $block_content; } } email-editor/src/Integrations/Core/Renderer/Blocks/class-social-links.php 0000777 00000036342 15252010675 0022455 0 ustar 00 <?php /** * This file is part of the WooCommerce Email Editor package * * @package Automattic\WooCommerce\EmailEditor */ declare( strict_types = 1 ); namespace Automattic\WooCommerce\EmailEditor\Integrations\Core\Renderer\Blocks; use Automattic\WooCommerce\EmailEditor\Engine\Renderer\ContentRenderer\Rendering_Context; use Automattic\WooCommerce\EmailEditor\Integrations\Utils\Social_Links_Helper; use Automattic\WooCommerce\EmailEditor\Integrations\Utils\Table_Wrapper_Helper; /** * Renders the social links block. */ class Social_Links extends Abstract_Block_Renderer { /** * Cache of the core social link services. * * @var array<string, array> */ private $core_social_link_services_cache = array(); /** * Supported image types. * * @var array<string> */ private $supported_image_types = array( 'white', 'brand' ); /** * Renders the block content. * * @param string $block_content Block content. * @param array $parsed_block Parsed block. * @param Rendering_Context $rendering_context Rendering context. * @return string */ protected function render_content( $block_content, array $parsed_block, Rendering_Context $rendering_context ): string { $attrs = $parsed_block['attrs'] ?? array(); $inner_blocks = $parsed_block['innerBlocks'] ?? array(); $content = ''; foreach ( $inner_blocks as $block ) { $content .= $this->generate_social_link_content( $block, $attrs ); } return str_replace( '{social_links_content}', $content, $this->get_block_wrapper( $block_content, $parsed_block ) ); } /** * Generates the social link content. * * @param array $block The block data. * @param array $parent_block_attrs The parent block attributes. * @return string The generated content. */ private function generate_social_link_content( $block, $parent_block_attrs ) { $service_name = $block['attrs']['service'] ?? ''; $service_url = $block['attrs']['url'] ?? ''; $label = $block['attrs']['label'] ?? ''; if ( empty( $service_name ) || empty( $service_url ) ) { return ''; } /** * Prepend emails with `mailto:` if not set. * The `is_email` returns false for emails with schema. */ if ( is_email( $service_url ) ) { $service_url = 'mailto:' . antispambot( $service_url ); } /** * Prepend URL with https:// if it doesn't appear to contain a scheme * and it's not a relative link or a fragment. */ if ( ! wp_parse_url( $service_url, PHP_URL_SCHEME ) && ! str_starts_with( $service_url, '//' ) && ! str_starts_with( $service_url, '#' ) ) { $service_url = 'https://' . $service_url; } $open_in_new_tab = $parent_block_attrs['openInNewTab'] ?? false; $show_labels = $parent_block_attrs['showLabels'] ?? false; $size = $parent_block_attrs['size'] ?? Social_Links_Helper::get_default_social_link_size(); $service_brand_color = Social_Links_Helper::get_service_brand_color( $service_name ); $icon_color_value = $parent_block_attrs['iconColorValue'] ?? '#ffffff'; // use white as default icon color. $icon_background_color_value = $parent_block_attrs['iconBackgroundColorValue'] ?? ''; $is_logos_only = strpos( $parent_block_attrs['className'] ?? '', 'is-style-logos-only' ) !== false; $is_pill_shape = strpos( $parent_block_attrs['className'] ?? '', 'is-style-pill-shape' ) !== false; if ( ! $is_logos_only && Social_Links_Helper::detect_whiteish_color( $icon_color_value ) && ( Social_Links_Helper::detect_whiteish_color( $icon_background_color_value ) || empty( $icon_background_color_value ) ) ) { // If the icon color is white and the background color is white or empty, use the service brand color for the icon background color. $icon_background_color_value = ! empty( $service_brand_color ) ? $service_brand_color : '#000'; } if ( $is_logos_only ) { // logos only mode does not need background color. We also don't really need the icon color (we can't change png image color anyways). // We set it so that the label text color will reflect the service brand color. $icon_color_value = ! empty( $service_brand_color ) ? $service_brand_color : '#000'; } $icon_size = Social_Links_Helper::get_social_link_size_option_value( $size ); $service_icon_url = $this->get_service_icon_url( $service_name, $is_logos_only ? 'brand' : 'white' ); $service_label = ''; if ( $show_labels ) { $text = ! empty( $label ) ? trim( $label ) : ''; $service_label = $text ? $text : block_core_social_link_get_name( $service_name ); } $main_table_styles = $this->compile_css( array( 'background-color' => $icon_background_color_value, 'border-radius' => '9999px', 'display' => 'inline-table', 'float' => 'none', ) ); // divide the icon value by 2 to get the font size. $font_size_value = (int) rtrim( $icon_size, 'px' ); $font_size = ( $font_size_value / 2 ) + 1; // inline with core styles. $text_font_size = "{$font_size}px"; $anchor_styles = $this->compile_css( array( 'color' => $icon_color_value, 'text-decoration' => 'none', 'text-transform' => 'none', 'font-size' => $text_font_size, ) ); $anchor_html = sprintf( ' style="%s" ', esc_attr( $anchor_styles ) ); if ( $open_in_new_tab ) { $anchor_html .= ' rel="noopener nofollow" target="_blank" '; } $row_container_styles = array( 'display' => 'block', 'padding' => '0.25em', ); if ( $is_pill_shape ) { $row_container_styles['padding-left'] = '17px'; $row_container_styles['padding-right'] = '17px'; } $row_container_styles = $this->compile_css( $row_container_styles ); // Generate the icon content. $icon_content = sprintf( '<a href="%1$s" %2$s class="wp-block-social-link-anchor"> <img height="%3$s" src="%4$s" style="display:block;margin-right:0;" width="%3$s" alt="%5$s"> </a>', esc_url( $service_url ), $anchor_html, esc_attr( $icon_size ), esc_url( $service_icon_url ), // translators: %s is the social service name. sprintf( __( '%s icon', 'woocommerce' ), $service_name ) ); $icon_content = Table_Wrapper_Helper::render_table_wrapper( $icon_content, array(), array( 'style' => 'vertical-align:middle;' ) ); $icon_content = Table_Wrapper_Helper::render_table_cell( $icon_content, array( 'style' => sprintf( 'vertical-align:middle;font-size:%s;', $text_font_size ) ) ); // Generate the label content if needed. $label_content = ''; if ( $service_label ) { $label_content = sprintf( '<a href="%1$s" %2$s class="wp-block-social-link-anchor"> <span style="margin-left:.5em;margin-right:.5em"> %3$s </span> </a>', esc_url( $service_url ), $anchor_html, esc_html( $service_label ) ); $label_cell_style = sprintf( 'vertical-align:middle;padding-left:6px;padding-right:6px;font-size:%s;', $text_font_size ); $label_content = Table_Wrapper_Helper::render_table_cell( $label_content, array( 'style' => $label_cell_style ) ); } // Combine icon and label tables. $social_link_content = $icon_content . $label_content; // Create the main social link table. $main_table_attrs = array( 'align' => 'center', 'style' => $main_table_styles, ); $main_row_attrs = array( 'style' => $row_container_styles, ); $main_table = Table_Wrapper_Helper::render_table_wrapper( $social_link_content, $main_table_attrs, array(), $main_row_attrs, false ); return Table_Wrapper_Helper::render_outlook_table_cell( $main_table ); } /** * Gets the block wrapper. * * @param string $block_content The block content. * @param array $parsed_block The parsed block. * @return string The block wrapper HTML. */ private function get_block_wrapper( $block_content, $parsed_block ) { $content = $this->adjust_block_content( $block_content, $parsed_block ); $table_styles = $content['table_styles']; $classes = $content['classes']; $compiled_styles = $content['compiled_styles']; $align = $content['align']; $table_attrs = array( 'class' => 'wp-block-social-links', 'style' => $table_styles . ' vertical-align:top;', 'width' => '100%', ); $cell_attrs = array( 'class' => $classes, 'style' => $compiled_styles, 'align' => $align, ); $row_attrs = array( 'role' => 'presentation', ); $inner_content = Table_Wrapper_Helper::render_outlook_table_wrapper( '{social_links_content}', array( 'align' => 'center' ), array(), array(), false ); $main_wrapper = Table_Wrapper_Helper::render_table_wrapper( $inner_content, $table_attrs, $cell_attrs, $row_attrs ); return Table_Wrapper_Helper::render_outlook_table_wrapper( $main_wrapper, array( 'align' => 'center' ) ); } /** * Adjusts the block content. * Returns css classes and styles compatible with email clients. * * @param string $block_content The block content. * @param array $parsed_block The parsed block. * @return array The adjusted block content. */ private function adjust_block_content( $block_content, $parsed_block ) { $block_content = $this->adjust_style_attribute( $block_content ); $block_attributes = wp_parse_args( $parsed_block['attrs'] ?? array(), array( 'textAlign' => 'left', 'style' => array(), ) ); $html = new \WP_HTML_Tag_Processor( $block_content ); $classes = 'wp-block-social-links'; if ( $html->next_tag() ) { /** @var string $block_classes */ // phpcs:ignore Generic.Commenting.DocComment.MissingShort -- used for phpstan $block_classes = $html->get_attribute( 'class' ) ?? ''; $classes .= ' ' . $block_classes; // remove has-background to prevent double padding applied for wrapper and inner element. $block_classes = str_replace( 'has-background', '', $block_classes ); // remove border related classes because we handle border on wrapping table cell. $block_classes = preg_replace( '/[a-z-]+-border-[a-z-]+/', '', $block_classes ); /** @var string $block_classes */ // phpcs:ignore Generic.Commenting.DocComment.MissingShort -- used for phpstan $html->set_attribute( 'class', trim( $block_classes ) ); $block_content = $html->get_updated_html(); } $block_styles = $this->get_styles_from_block( array( 'color' => $block_attributes['style']['color'] ?? array(), 'spacing' => $block_attributes['style']['spacing'] ?? array(), 'typography' => $block_attributes['style']['typography'] ?? array(), 'border' => $block_attributes['style']['border'] ?? array(), ) ); $styles = array( 'min-width' => '100%', // prevent Gmail App from shrinking the table on mobile devices. 'vertical-align' => 'middle', 'word-break' => 'break-word', ); $styles['text-align'] = 'left'; if ( ! empty( $parsed_block['attrs']['textAlign'] ) ) { // in this case, textAlign needs to be one of 'left', 'center', 'right'. $styles['text-align'] = $parsed_block['attrs']['textAlign']; } elseif ( in_array( $parsed_block['attrs']['align'] ?? null, array( 'left', 'center', 'right' ), true ) ) { $styles['text-align'] = $parsed_block['attrs']['align']; } $compiled_styles = $this->compile_css( $block_styles['declarations'], $styles ); $table_styles = 'border-collapse: separate;'; // Needed because of border radius. return array( 'table_styles' => $table_styles, 'classes' => $classes, 'compiled_styles' => $compiled_styles, 'align' => $styles['text-align'], 'block_content' => $block_content, ); } /** * 1) We need to remove padding because we render padding on wrapping table cell * 2) We also need to replace font-size to avoid clamp() because clamp() is not supported in many email clients. * The font size values is automatically converted to clamp() when WP site theme is configured to use fluid layouts. * Currently (WP 6.5), there is no way to disable this behavior. * * @param string $block_content Block content. */ private function adjust_style_attribute( string $block_content ): string { $html = new \WP_HTML_Tag_Processor( $block_content ); if ( $html->next_tag() ) { $element_style_value = $html->get_attribute( 'style' ); $element_style = isset( $element_style_value ) ? strval( $element_style_value ) : ''; // Padding may contain value like 10px or variable like var(--spacing-10). $element_style = preg_replace( '/padding[^:]*:.?[0-9a-z-()]+;?/', '', $element_style ); // Remove border styles. We apply border styles on the wrapping table cell. $element_style = preg_replace( '/border[^:]*:.?[0-9a-z-()#]+;?/', '', strval( $element_style ) ); // We define the font-size on the wrapper element, but we need to keep font-size definition here // to prevent CSS Inliner from adding a default value and overriding the value set by user, which is on the wrapper element. // The value provided by WP uses clamp() function which is not supported in many email clients. $element_style = preg_replace( '/font-size:[^;]+;?/', 'font-size: inherit;', strval( $element_style ) ); /** @var string $element_style */ // phpcs:ignore Generic.Commenting.DocComment.MissingShort -- used for phpstan $html->set_attribute( 'style', esc_attr( $element_style ) ); $block_content = $html->get_updated_html(); } return $block_content; } /** * Gets the service icon URL. * * Default image type is 'white'. * * @param string $service The service name. * @param string $image_type The image type. e.g 'white', 'brand'. * @return string The service icon URL. */ public function get_service_icon_url( $service, $image_type = '' ) { $image_type = empty( $image_type ) ? 'white' : $image_type; $service = empty( $service ) ? '' : strtolower( $service ); if ( empty( $this->core_social_link_services_cache ) ) { $services = block_core_social_link_services(); $this->core_social_link_services_cache = is_array( $services ) ? $services : array(); } if ( ! isset( $this->core_social_link_services_cache[ $service ] ) ) { // not in the list of core services. return ''; } if ( ! in_array( $image_type, $this->supported_image_types, true ) ) { return ''; } // Get URL to icons/service.png. $service_icon_url = $this->get_service_png_url( $service, $image_type ); if ( $service_icon_url && ! file_exists( $this->get_service_png_path( $service, $image_type ) ) ) { // The image file does not exist. return ''; } return $service_icon_url; } /** * Gets the service PNG URL. * * @param string $service The service name. * @param string $image_type The image type. e.g 'white', 'brand'. * @return string The service PNG URL. */ public function get_service_png_url( $service, $image_type = 'white' ) { if ( empty( $service ) ) { return ''; } $image_type = empty( $image_type ) ? 'white' : $image_type; $file_name = "/icons/{$service}/{$service}-{$image_type}.png"; return plugins_url( $file_name, __FILE__ ); } /** * Gets the service PNG path. * * @param string $service The service name. * @param string $image_type The image type. e.g 'white', 'brand'. * @return string The service PNG path. */ public function get_service_png_path( $service, $image_type = 'white' ) { if ( empty( $service ) ) { return ''; } $image_type = empty( $image_type ) ? 'white' : $image_type; $file_name = "/icons/{$service}/{$service}-{$image_type}.png"; return __DIR__ . $file_name; } } email-editor/src/Integrations/Core/Renderer/Blocks/class-button.php 0000777 00000010502 15252010675 0021366 0 ustar 00 <?php /** * This file is part of the WooCommerce Email Editor package. * * @package Automattic\WooCommerce\EmailEditor */ declare( strict_types = 1 ); namespace Automattic\WooCommerce\EmailEditor\Integrations\Core\Renderer\Blocks; use Automattic\WooCommerce\EmailEditor\Engine\Renderer\ContentRenderer\Rendering_Context; use Automattic\WooCommerce\EmailEditor\Integrations\Utils\Dom_Document_Helper; use Automattic\WooCommerce\EmailEditor\Integrations\Utils\Table_Wrapper_Helper; use Automattic\WooCommerce\EmailEditor\Integrations\Utils\Styles_Helper; /** * Renders a button block. * * @see https://www.activecampaign.com/blog/email-buttons * @see https://documentation.mjml.io/#mj-button */ class Button extends Abstract_Block_Renderer { /** * Get styles for the wrapper element. * * @param array $block_attributes Block attributes. * @param Rendering_Context $rendering_context Rendering context. * @return array */ private function get_wrapper_styles( array $block_attributes, Rendering_Context $rendering_context ) { $block_styles = Styles_Helper::get_block_styles( $block_attributes, $rendering_context, array( 'border', 'background-color', 'color', 'typography', 'spacing' ) ); return Styles_Helper::extend_block_styles( $block_styles, array( 'word-break' => 'break-word', 'display' => 'block', ) ); } /** * Get styles for the link element. * * @param array $block_attributes Block attributes. * @param Rendering_Context $rendering_context Rendering context. * @return array */ private function get_link_styles( array $block_attributes, Rendering_Context $rendering_context ) { $block_styles = Styles_Helper::get_block_styles( $block_attributes, $rendering_context, array( 'color', 'typography' ) ); return Styles_Helper::extend_block_styles( $block_styles, array( 'display' => 'block' ) ); } /** * Renders the block. * * @param string $block_content Block content. * @param array $parsed_block Parsed block. * @param Rendering_Context $rendering_context Rendering context. * @return string */ public function render( string $block_content, array $parsed_block, Rendering_Context $rendering_context ): string { return $this->render_content( $block_content, $parsed_block, $rendering_context ); } /** * Renders the block content. * * @param string $block_content Block content. * @param array $parsed_block Parsed block. * @param Rendering_Context $rendering_context Rendering context. * @return string */ protected function render_content( string $block_content, array $parsed_block, Rendering_Context $rendering_context ): string { if ( empty( $parsed_block['innerHTML'] ) ) { return ''; } $dom_helper = new Dom_Document_Helper( $parsed_block['innerHTML'] ); $block_classname = $dom_helper->get_attribute_value_by_tag_name( 'div', 'class' ) ?? ''; $button_link = $dom_helper->find_element( 'a' ); if ( ! $button_link ) { return ''; } $button_text = $dom_helper->get_element_inner_html( $button_link ) ? $dom_helper->get_element_inner_html( $button_link ) : ''; $button_url = $button_link->getAttribute( 'href' ) ? $button_link->getAttribute( 'href' ) : '#'; $block_attributes = wp_parse_args( $parsed_block['attrs'] ?? array(), array( 'width' => '', 'style' => array(), 'textAlign' => 'center', 'backgroundColor' => '', 'textColor' => '', ) ); $wrapper_styles = $this->get_wrapper_styles( $block_attributes, $rendering_context ); $link_styles = $this->get_link_styles( $block_attributes, $rendering_context ); $table_attrs = array( 'style' => 'width:' . ( $block_attributes['width'] ? '100%' : 'auto' ) . ';', ); $cell_attrs = array( 'class' => $wrapper_styles['classnames'] . ' ' . $block_classname, 'style' => $wrapper_styles['css'], 'align' => $block_attributes['textAlign'], 'valign' => 'middle', 'role' => 'presentation', ); $button_content = sprintf( '<a class="button-link %1$s" style="%2$s" href="%3$s" target="_blank">%4$s</a>', esc_attr( $link_styles['classnames'] ), esc_attr( $link_styles['css'] ), esc_url( $button_url ), $button_text ); return Table_Wrapper_Helper::render_table_wrapper( $button_content, $table_attrs, $cell_attrs ); } } email-editor/src/Integrations/Core/Renderer/Blocks/class-social-link.php 0000777 00000001617 15252010675 0022267 0 ustar 00 <?php /** * This file is part of the WooCommerce Email Editor package * * @package Automattic\WooCommerce\EmailEditor */ declare( strict_types = 1 ); namespace Automattic\WooCommerce\EmailEditor\Integrations\Core\Renderer\Blocks; use Automattic\WooCommerce\EmailEditor\Engine\Renderer\ContentRenderer\Rendering_Context; /** * Renders a social link block. */ class Social_Link extends Abstract_Block_Renderer { /** * Renders the block content. * * @param string $block_content Block content. * @param array $parsed_block Parsed block. * @param Rendering_Context $rendering_context Rendering context. * @return string */ protected function render_content( $block_content, array $parsed_block, Rendering_Context $rendering_context ): string { // We are not using this because the blocks are rendered in the Social_Links block class. return $block_content; } } email-editor/src/Integrations/Core/Renderer/Blocks/class-image.php 0000777 00000041637 15252010675 0021152 0 ustar 00 <?php /** * This file is part of the WooCommerce Email Editor package. * * @package Automattic\WooCommerce\EmailEditor */ declare( strict_types = 1 ); namespace Automattic\WooCommerce\EmailEditor\Integrations\Core\Renderer\Blocks; use Automattic\WooCommerce\EmailEditor\Engine\Renderer\ContentRenderer\Rendering_Context; use Automattic\WooCommerce\EmailEditor\Integrations\Utils\Dom_Document_Helper; use Automattic\WooCommerce\EmailEditor\Integrations\Utils\Styles_Helper; use Automattic\WooCommerce\EmailEditor\Integrations\Utils\Table_Wrapper_Helper; /** * Renders an image block. */ class Image extends Abstract_Block_Renderer { /** * Renders the block content * * @param string $block_content Block content. * @param array $parsed_block Parsed block. * @param Rendering_Context $rendering_context Rendering context. * @return string */ protected function render_content( string $block_content, array $parsed_block, Rendering_Context $rendering_context ): string { $parsed_html = $this->parse_block_content( $block_content ); if ( ! $parsed_html ) { return ''; } $image_url = $parsed_html['imageUrl']; $image = $parsed_html['image']; $caption = $parsed_html['caption']; $class = $parsed_html['class']; $anchor_tag_href = $parsed_html['anchor_tag_href']; $parsed_block = $this->add_image_size_when_missing( $parsed_block, $image_url ); $image = $this->add_image_dimensions( $image, $parsed_block ); $image_with_wrapper = str_replace( array( '{image_content}', '{caption_content}' ), array( $image, $caption ), $this->get_block_wrapper( $parsed_block, $rendering_context, $caption, $anchor_tag_href ) ); $image_with_wrapper = $this->apply_rounded_style( $image_with_wrapper, $parsed_block ); $image_with_wrapper = $this->apply_image_border_style( $image_with_wrapper, $parsed_block, $class ); return $image_with_wrapper; } /** * Apply rounded style to the image. * * @param string $block_content Block content. * @param array $parsed_block Parsed block. */ private function apply_rounded_style( string $block_content, array $parsed_block ): string { // Because the isn't an attribute for definition of rounded style, we have to check the class name. if ( isset( $parsed_block['attrs']['className'] ) && strpos( $parsed_block['attrs']['className'], 'is-style-rounded' ) !== false ) { // If the image should be in a circle, we need to set the border-radius to 9999px to make it the same as is in the editor // This style is applied to both wrapper and the image. $block_content = $this->remove_style_attribute_from_element( $block_content, array( 'tag_name' => 'td', 'class_name' => 'email-image-cell', ), 'border-radius' ); $block_content = $this->add_style_to_element( $block_content, array( 'tag_name' => 'td', 'class_name' => 'email-image-cell', ), 'border-radius: 9999px;' ); $block_content = $this->remove_style_attribute_from_element( $block_content, array( 'tag_name' => 'img' ), 'border-radius' ); $block_content = $this->add_style_to_element( $block_content, array( 'tag_name' => 'img' ), 'border-radius: 9999px;' ); } return $block_content; } /** * When the width is not set, it's important to get it for the image to be displayed correctly * * @param array $parsed_block Parsed block. * @param string $image_url Image URL. */ private function add_image_size_when_missing( array $parsed_block, string $image_url ): array { if ( isset( $parsed_block['attrs']['width'] ) ) { return $parsed_block; } // Can't determine any width let's go with 100%. if ( ! isset( $parsed_block['email_attrs']['width'] ) ) { $parsed_block['attrs']['width'] = '100%'; return $parsed_block; } $max_width = Styles_Helper::parse_value( $parsed_block['email_attrs']['width'] ); $image_size = null; if ( $image_url ) { // Try to extract width from URL query parameter if it exists. $parsed_url = wp_parse_url( $image_url ); if ( isset( $parsed_url['query'] ) ) { parse_str( $parsed_url['query'], $query_params ); if ( isset( $query_params['w'] ) && is_numeric( $query_params['w'] ) && $query_params['w'] > 0 ) { $image_size = (int) $query_params['w']; } } // Next we check the attachment data if it has an ID. if ( ! isset( $image_size ) ) { $attachment_id = $parsed_block['attrs']['id'] ?? null; if ( $attachment_id ) { $size_slug = $parsed_block['attrs']['sizeSlug'] ?? 'large'; // Check the metadata first. $metadata = wp_get_attachment_metadata( $attachment_id ); if ( $metadata ) { if ( isset( $metadata['sizes'][ $size_slug ]['width'] ) ) { $image_size = (int) $metadata['sizes'][ $size_slug ]['width']; } elseif ( 'full' === $size_slug && isset( $metadata['width'] ) ) { $image_size = (int) $metadata['width']; } } // Try to get dimensions from wp_get_attachment_image_src if metadata didn't have it. if ( ! isset( $image_size ) ) { $image_src = wp_get_attachment_image_src( $attachment_id, $size_slug ); if ( $image_src && isset( $image_src[1] ) ) { $image_size = (int) $image_src[1]; } } } } // Fallback to wp_getimagesize if we still don't have a size. if ( ! isset( $image_size ) ) { $upload_dir = wp_upload_dir(); $image_path = str_replace( $upload_dir['baseurl'], $upload_dir['basedir'], $image_url ); $result = wp_getimagesize( $image_path ); if ( $result ) { $image_size = (int) $result[0]; } } } // Use the found image size or fall back to max_width. $width = isset( $image_size ) ? min( $image_size, $max_width ) : $max_width; $parsed_block['attrs']['width'] = "{$width}px"; return $parsed_block; } /** * Apply border style to the image. * * @param string $block_content Block content. * @param array $parsed_block Parsed block. * @param string $class_name Class name. */ private function apply_image_border_style( string $block_content, array $parsed_block, string $class_name ): string { // Getting individual border properties. $border_styles = wp_style_engine_get_styles( array( 'border' => $parsed_block['attrs']['style']['border'] ?? array() ) ); $border_styles = $border_styles['declarations'] ?? array(); if ( ! empty( $border_styles ) ) { $border_styles['border-style'] = 'solid'; $border_styles['box-sizing'] = 'border-box'; } $border_element_tag = array( 'tag_name' => 'td', 'class_name' => 'email-image-cell', ); $content_with_border_styles = $this->add_style_to_element( $block_content, $border_element_tag, \WP_Style_Engine::compile_css( $border_styles, '' ) ); // Remove border styles from the image HTML tag. $content_with_border_styles = $this->remove_style_attribute_from_element( $content_with_border_styles, array( 'tag_name' => 'img' ), 'border-style' ); $content_with_border_styles = $this->remove_style_attribute_from_element( $content_with_border_styles, array( 'tag_name' => 'img' ), 'border-width' ); $content_with_border_styles = $this->remove_style_attribute_from_element( $content_with_border_styles, array( 'tag_name' => 'img' ), 'border-color' ); $content_with_border_styles = $this->remove_style_attribute_from_element( $content_with_border_styles, array( 'tag_name' => 'img' ), 'border-radius' ); // Add Border related classes to proper element. This is required for inlined border-color styles when defined via class. $border_classes = array_filter( explode( ' ', $class_name ), function ( $class_name ) { return strpos( $class_name, 'border' ) !== false; } ); $html = new \WP_HTML_Tag_Processor( $content_with_border_styles ); if ( $html->next_tag( $border_element_tag ) ) { $class_name = $html->get_attribute( 'class' ) ?? ''; $border_classes[] = $class_name; $html->set_attribute( 'class', implode( ' ', $border_classes ) ); } return $html->get_updated_html(); } /** * Settings width and height attributes for images is important for MS Outlook. * * @param string $block_content Block content. * @param array $parsed_block Parsed block. */ private function add_image_dimensions( string $block_content, array $parsed_block ): string { $html = new \WP_HTML_Tag_Processor( $block_content ); if ( $html->next_tag( array( 'tag_name' => 'img' ) ) ) { // Getting height from styles and if it's set, we set the height attribute. /** @var string $styles */ // phpcs:ignore Generic.Commenting.DocComment.MissingShort -- used for phpstan $styles = $html->get_attribute( 'style' ) ?? ''; $styles = Styles_Helper::parse_styles_to_array( $styles ); $height = $styles['height'] ?? null; if ( $height && 'auto' !== $height ) { $height = Styles_Helper::parse_value( $height ); /* @phpstan-ignore-next-line Wrong annotation for parameter in WP. */ $html->set_attribute( 'height', esc_attr( $height ) ); } if ( isset( $parsed_block['attrs']['width'] ) ) { $width = Styles_Helper::parse_value( $parsed_block['attrs']['width'] ); /* @phpstan-ignore-next-line Wrong annotation for parameter in WP. */ $html->set_attribute( 'width', esc_attr( $width ) ); } $block_content = $html->get_updated_html(); } return $block_content; } /** * This method configure the font size of the caption because it's set to 0 for the parent element to avoid unexpected white spaces * We try to use font-size passed down from the parent element $parsedBlock['email_attrs']['font-size'], but if it's not set, we use the default font-size from the email theme. * * @param Rendering_Context $rendering_context Rendering context. * @param array $parsed_block Parsed block. */ private function get_caption_styles( Rendering_Context $rendering_context, array $parsed_block ): string { $theme_data = $rendering_context->get_theme_json()->get_data(); $styles = array( 'text-align' => isset( $parsed_block['attrs']['align'] ) ? 'center' : 'left', ); $styles['font-size'] = $parsed_block['email_attrs']['font-size'] ?? $theme_data['styles']['typography']['fontSize']; return \WP_Style_Engine::compile_css( $styles, '' ); } /** * Based on MJML <mj-image> but because MJML doesn't support captions, our solution is a bit different * * @param array $parsed_block Parsed block. * @param Rendering_Context $rendering_context Rendering context. * @param string|null $caption Caption. * @param string|null $anchor_tag_href Anchor tag href. */ private function get_block_wrapper( array $parsed_block, Rendering_Context $rendering_context, ?string $caption, ?string $anchor_tag_href ): string { $styles = array( 'border-collapse' => 'collapse', 'border-spacing' => '0px', 'font-size' => '0px', 'vertical-align' => 'top', 'width' => '100%', ); $width = $parsed_block['attrs']['width'] ?? '100%'; $wrapper_width = ( $width && '100%' !== $width ) ? $width : 'auto'; $wrapper_styles = $styles; $wrapper_styles['width'] = $wrapper_width; $wrapper_styles['border-collapse'] = 'separate'; // Needed because of border radius. $caption_html = ''; if ( $caption ) { // When the image is not aligned, the wrapper is set to 100% width due to caption that can be longer than the image. $caption_width = isset( $parsed_block['attrs']['align'] ) ? ( $parsed_block['attrs']['width'] ?? '100%' ) : '100%'; $caption_wrapper_styles = $styles; $caption_wrapper_styles['width'] = $caption_width; $caption_styles = $this->get_caption_styles( $rendering_context, $parsed_block ); $caption_table_attrs = array( 'class' => 'email-table-with-width', 'style' => \WP_Style_Engine::compile_css( $caption_wrapper_styles, '' ), 'width' => $caption_width, ); $caption_cell_attrs = array( 'style' => $caption_styles, ); $caption_html = Table_Wrapper_Helper::render_table_wrapper( '{caption_content}', $caption_table_attrs, $caption_cell_attrs ); } $styles['width'] = '100%'; $align = $parsed_block['attrs']['align'] ?? 'left'; $table_attrs = array( 'style' => \WP_Style_Engine::compile_css( $styles, '' ), 'width' => '100%', ); $cell_attrs = array( 'align' => $align, ); $image_table_attrs = array( 'class' => 'email-table-with-width', 'style' => \WP_Style_Engine::compile_css( $wrapper_styles, '' ), 'width' => $wrapper_width, ); $image_cell_attrs = array( 'class' => 'email-image-cell', 'style' => 'overflow: hidden;', ); $image_content = '{image_content}'; if ( $anchor_tag_href ) { $image_content = sprintf( '<a href="%s" rel="noopener nofollow" target="_blank">%s</a>', esc_url( $anchor_tag_href ), '{image_content}' ); } $image_html = Table_Wrapper_Helper::render_table_wrapper( $image_content, $image_table_attrs, $image_cell_attrs ); $inner_content = $image_html . $caption_html; return Table_Wrapper_Helper::render_table_wrapper( $inner_content, $table_attrs, $cell_attrs ); } /** * Add style to the element. * * @param string $block_content Block content. * @param array{tag_name: string, class_name?: string} $tag Tag to add style to. * @param string $style Style to add. */ private function add_style_to_element( $block_content, array $tag, string $style ): string { $html = new \WP_HTML_Tag_Processor( $block_content ); if ( $html->next_tag( $tag ) ) { /** @var string $element_style */ // phpcs:ignore Generic.Commenting.DocComment.MissingShort -- used for phpstan $element_style = $html->get_attribute( 'style' ) ?? ''; $element_style = ! empty( $element_style ) ? ( rtrim( $element_style, ';' ) . ';' ) : ''; // Adding semicolon if it's missing. $element_style .= $style; $html->set_attribute( 'style', esc_attr( $element_style ) ); $block_content = $html->get_updated_html(); } return $block_content; } /** * Remove style attribute from the element. * * @param string $block_content Block content. * @param array{tag_name: string, class_name?: string} $tag Tag to remove style from. * @param string $style_name Name of the style to remove. */ private function remove_style_attribute_from_element( $block_content, array $tag, string $style_name ): string { $html = new \WP_HTML_Tag_Processor( $block_content ); if ( $html->next_tag( $tag ) ) { /** @var string $element_style */ // phpcs:ignore Generic.Commenting.DocComment.MissingShort -- used for phpstan $element_style = $html->get_attribute( 'style' ) ?? ''; $element_style = preg_replace( '/' . preg_quote( $style_name, '/' ) . '\s*:\s*[^;]+;?/', '', $element_style ); $html->set_attribute( 'style', esc_attr( strval( $element_style ) ) ); $block_content = $html->get_updated_html(); } return $block_content; } /** * Parse block content to get image URL, image HTML and caption HTML. * * @param string $block_content Block content. * @return array{imageUrl: string, image: string, caption: string, class: string, anchor_tag_href: string}|null */ private function parse_block_content( string $block_content ): ?array { // If block's image is not set, we don't need to parse the content. if ( empty( $block_content ) ) { return null; } $dom_helper = new Dom_Document_Helper( $block_content ); $figure_tag = $dom_helper->find_element( 'figure' ); if ( ! $figure_tag ) { return null; } $img_tag = $dom_helper->find_element( 'img' ); if ( ! $img_tag ) { return null; } $image_src = $dom_helper->get_attribute_value( $img_tag, 'src' ); $image_class = $dom_helper->get_attribute_value( $img_tag, 'class' ); $image_html = $dom_helper->get_outer_html( $img_tag ); $figcaption = $dom_helper->find_element( 'figcaption' ); $figcaption_html = $figcaption ? $dom_helper->get_outer_html( $figcaption ) : ''; $figcaption_html = str_replace( array( '<figcaption', '</figcaption>' ), array( '<span', '</span>' ), $figcaption_html ); $anchor_tag = $dom_helper->find_element( 'a' ); $anchor_tag_href = $anchor_tag ? $dom_helper->get_attribute_value( $anchor_tag, 'href' ) : ''; return array( 'imageUrl' => $image_src ? $image_src : '', 'image' => $this->cleanup_image_html( $image_html ), 'caption' => $figcaption_html ? $figcaption_html : '', 'class' => $image_class ? $image_class : '', 'anchor_tag_href' => $anchor_tag_href ? $anchor_tag_href : '', ); } /** * Cleanup image HTML. * * @param string $content_html Content HTML. */ private function cleanup_image_html( string $content_html ): string { $html = new \WP_HTML_Tag_Processor( $content_html ); if ( $html->next_tag( array( 'tag_name' => 'img' ) ) ) { $html->remove_attribute( 'srcset' ); $html->remove_attribute( 'class' ); } return $html->get_updated_html(); } } email-editor/src/Integrations/Core/Renderer/Blocks/class-text.php 0000777 00000014155 15252010675 0021047 0 ustar 00 <?php /** * This file is part of the WooCommerce Email Editor package. * * @package Automattic\WooCommerce\EmailEditor */ declare( strict_types = 1 ); namespace Automattic\WooCommerce\EmailEditor\Integrations\Core\Renderer\Blocks; use Automattic\WooCommerce\EmailEditor\Engine\Renderer\ContentRenderer\Rendering_Context; use Automattic\WooCommerce\EmailEditor\Integrations\Utils\Styles_Helper; use Automattic\WooCommerce\EmailEditor\Integrations\Utils\Table_Wrapper_Helper; /** * This renderer covers both core/paragraph, core/heading and core/site-title blocks. */ class Text extends Abstract_Block_Renderer { /** * Renders the block content. * * @param string $block_content Block content. * @param array $parsed_block Parsed block. * @param Rendering_Context $rendering_context Rendering context. * @return string */ protected function render_content( string $block_content, array $parsed_block, Rendering_Context $rendering_context ): string { // Do not render empty blocks. if ( empty( trim( wp_strip_all_tags( $block_content ) ) ) ) { return ''; } $block_content = $this->adjustStyleAttribute( $block_content ); $block_attributes = wp_parse_args( $parsed_block['attrs'] ?? array(), array( 'textAlign' => 'left', 'style' => array(), ) ); $html = new \WP_HTML_Tag_Processor( $block_content ); $classes = 'email-text-block'; $alignment_from_class = null; if ( $html->next_tag() ) { /** @var string $block_classes */ // phpcs:ignore Generic.Commenting.DocComment.MissingShort -- used for phpstan $block_classes = $html->get_attribute( 'class' ) ?? ''; $classes .= ' ' . $block_classes; // Extract text alignment from has-text-align-* classes before they're potentially modified. $class_attr = (string) $block_classes; if ( false !== strpos( $class_attr, 'has-text-align-center' ) ) { $alignment_from_class = 'center'; } elseif ( false !== strpos( $class_attr, 'has-text-align-right' ) ) { $alignment_from_class = 'right'; } elseif ( false !== strpos( $class_attr, 'has-text-align-left' ) ) { $alignment_from_class = 'left'; } // remove has-background to prevent double padding applied for wrapper and inner element. $block_classes = str_replace( 'has-background', '', $block_classes ); // remove border related classes because we handle border on wrapping table cell. $block_classes = preg_replace( '/[a-z-]+-border-[a-z-]+/', '', $block_classes ); /** @var string $block_classes */ // phpcs:ignore Generic.Commenting.DocComment.MissingShort -- used for phpstan $html->set_attribute( 'class', trim( $block_classes ) ); $block_content = $html->get_updated_html(); } $block_styles = Styles_Helper::get_block_styles( $block_attributes, $rendering_context, array( 'spacing', 'border', 'background-color', 'color', 'typography' ) ); $additional_styles = array( 'min-width' => '100%', // prevent Gmail App from shrinking the table on mobile devices. ); // Add fallback text color when no custom text color or preset text color is set. if ( empty( $block_styles['declarations']['color'] ) ) { $email_styles = $rendering_context->get_theme_styles(); $additional_styles['color'] = $parsed_block['email_attrs']['color'] ?? $email_styles['color']['text'] ?? '#000000'; // Fallback for the text color. } $additional_styles['text-align'] = 'left'; if ( ! empty( $parsed_block['attrs']['textAlign'] ) ) { // in this case, textAlign needs to be one of 'left', 'center', 'right'. $additional_styles['text-align'] = $parsed_block['attrs']['textAlign']; } elseif ( in_array( $parsed_block['attrs']['align'] ?? null, array( 'left', 'center', 'right' ), true ) ) { $additional_styles['text-align'] = $parsed_block['attrs']['align']; } elseif ( null !== $alignment_from_class ) { $additional_styles['text-align'] = $alignment_from_class; } $block_styles = Styles_Helper::extend_block_styles( $block_styles, $additional_styles ); $table_attrs = array( 'style' => 'border-collapse: separate;', // Needed because of border radius. 'width' => '100%', ); $cell_attrs = array( 'class' => $classes, 'style' => $block_styles['css'], 'align' => $additional_styles['text-align'], ); return Table_Wrapper_Helper::render_table_wrapper( $block_content, $table_attrs, $cell_attrs ); } /** * 1) We need to remove padding because we render padding on wrapping table cell * 2) We also need to replace font-size to avoid clamp() because clamp() is not supported in many email clients. * The font size values is automatically converted to clamp() when WP site theme is configured to use fluid layouts. * Currently (WP 6.5), there is no way to disable this behavior. * * @param string $block_content Block content. */ private function adjustStyleAttribute( string $block_content ): string { $html = new \WP_HTML_Tag_Processor( $block_content ); if ( $html->next_tag() ) { $element_style_value = $html->get_attribute( 'style' ); $element_style = isset( $element_style_value ) ? strval( $element_style_value ) : ''; // Padding may contain value like 10px or variable like var(--spacing-10). $element_style = preg_replace( '/padding[^:]*:.?[0-9a-z-()]+;?/', '', $element_style ); // Remove border styles. We apply border styles on the wrapping table cell. $element_style = preg_replace( '/border[^:]*:.?[0-9a-z-()#]+;?/', '', strval( $element_style ) ); // We define the font-size on the wrapper element, but we need to keep font-size definition here // to prevent CSS Inliner from adding a default value and overriding the value set by user, which is on the wrapper element. // The value provided by WP uses clamp() function which is not supported in many email clients. $element_style = preg_replace( '/font-size:[^;]+;?/', 'font-size: inherit;', strval( $element_style ) ); /** @var string $element_style */ // phpcs:ignore Generic.Commenting.DocComment.MissingShort -- used for phpstan $html->set_attribute( 'style', esc_attr( $element_style ) ); $block_content = $html->get_updated_html(); } return $block_content; } } email-editor/src/Integrations/Core/Renderer/Blocks/class-cover.php 0000777 00000022154 15252010675 0021177 0 ustar 00 <?php /** * This file is part of the WooCommerce Email Editor package * * @package Automattic\WooCommerce\EmailEditor */ declare( strict_types = 1 ); namespace Automattic\WooCommerce\EmailEditor\Integrations\Core\Renderer\Blocks; use Automattic\WooCommerce\EmailEditor\Engine\Renderer\ContentRenderer\Rendering_Context; use Automattic\WooCommerce\EmailEditor\Integrations\Utils\Table_Wrapper_Helper; use Automattic\WooCommerce\EmailEditor\Integrations\Utils\Styles_Helper; use Automattic\WooCommerce\EmailEditor\Integrations\Utils\Dom_Document_Helper; use Automattic\WooCommerce\EmailEditor\Integrations\Utils\Html_Processing_Helper; /** * Cover block renderer. * This renderer handles core/cover blocks with proper email-friendly HTML layout. */ class Cover extends Abstract_Block_Renderer { /** * Renders the cover block content using a table-based layout for email compatibility. * * @param string $block_content Block content. * @param array $parsed_block Parsed block. * @param Rendering_Context $rendering_context Rendering context. * @return string */ protected function render_content( string $block_content, array $parsed_block, Rendering_Context $rendering_context ): string { $block_attrs = $parsed_block['attrs'] ?? array(); $inner_blocks = $parsed_block['innerBlocks'] ?? array(); // Render all inner blocks content. $inner_content = ''; foreach ( $inner_blocks as $block ) { $inner_content .= render_block( $block ); } // If we don't have inner content, return empty. if ( empty( $inner_content ) ) { return ''; } // Build the email-friendly layout. $background_image = $this->extract_background_image( $block_attrs, $parsed_block['innerHTML'] ?? $block_content ); return $this->build_email_layout( $inner_content, $block_attrs, $block_content, $background_image, $rendering_context ); } /** * Build the email-friendly layout for cover blocks. * * @param string $inner_content Inner content. * @param array $block_attrs Block attributes. * @param string $block_content Original block content. * @param string $background_image Background image URL. * @param Rendering_Context $rendering_context Rendering context. * @return string Rendered HTML. */ private function build_email_layout( string $inner_content, array $block_attrs, string $block_content, string $background_image, Rendering_Context $rendering_context ): string { // Get original wrapper classes from block content. $original_wrapper_classname = ( new Dom_Document_Helper( $block_content ) )->get_attribute_value_by_tag_name( 'div', 'class' ) ?? ''; // Get background color information. $background_color = $this->get_background_color( $block_attrs, $rendering_context ); // Get block styles using the Styles_Helper. $block_styles = Styles_Helper::get_block_styles( $block_attrs, $rendering_context, array( 'padding', 'border', 'background-color' ) ); $default_styles = array( 'width' => '100%', 'border-collapse' => 'collapse', 'text-align' => 'center', ); // Add minimum height (use specified value or default). $min_height = $this->get_minimum_height( $block_attrs ); $default_styles['min-height'] = ! empty( $min_height ) ? $min_height : '430px'; $block_styles = Styles_Helper::extend_block_styles( $block_styles, $default_styles ); // Add background image to table styles if present. if ( ! empty( $background_image ) ) { $block_styles = Styles_Helper::extend_block_styles( $block_styles, array( 'background-image' => 'url("' . esc_url( $background_image ) . '")', 'background-size' => 'cover', 'background-position' => 'center', 'background-repeat' => 'no-repeat', ) ); } elseif ( ! empty( $background_color ) ) { // If no background image but there's a background color, use it. $block_styles = Styles_Helper::extend_block_styles( $block_styles, array( 'background-color' => $background_color, ) ); } // Apply class and style attributes to the wrapper table. $table_attrs = array( 'class' => 'email-block-cover ' . esc_attr( $original_wrapper_classname ), 'style' => $block_styles['css'], 'align' => 'center', 'width' => '100%', ); // Build the cover content without background (background is now on the table). $cover_content = $this->build_cover_content( $inner_content ); // Build individual table cell. $cell_attrs = array( 'valign' => 'middle', 'align' => 'center', ); $cell = Table_Wrapper_Helper::render_table_cell( $cover_content, $cell_attrs ); // Use render_cell = false to avoid wrapping in an extra <td>. return Table_Wrapper_Helper::render_table_wrapper( $cell, $table_attrs, array(), array(), false ); } /** * Extract background image from block attributes or HTML content. * * @param array $block_attrs Block attributes. * @param string $block_content Original block content. * @return string Background image URL or empty string. */ private function extract_background_image( array $block_attrs, string $block_content ): string { // First check block attributes for URL. if ( ! empty( $block_attrs['url'] ) ) { return esc_url( $block_attrs['url'] ); } // Fallback: use HTML API to find background image src. $html = new \WP_HTML_Tag_Processor( $block_content ); while ( $html->next_tag( array( 'tag_name' => 'img' ) ) ) { $class_attr = $html->get_attribute( 'class' ); // Check if this img tag has the wp-block-cover__image-background class. if ( is_string( $class_attr ) && false !== strpos( $class_attr, 'wp-block-cover__image-background' ) ) { $src = $html->get_attribute( 'src' ); if ( is_string( $src ) ) { return esc_url( $src ); } } } return ''; } /** * Get minimum height from block attributes. * * @param array $block_attrs Block attributes. * @return string Minimum height value or empty string. */ private function get_minimum_height( array $block_attrs ): string { // Check for minHeight attribute (legacy format). if ( ! empty( $block_attrs['minHeight'] ) ) { return Html_Processing_Helper::sanitize_dimension_value( $block_attrs['minHeight'] ); } // Check for style.dimensions.minHeight (WordPress 6.2+ format). if ( ! empty( $block_attrs['style']['dimensions']['minHeight'] ) ) { return Html_Processing_Helper::sanitize_dimension_value( $block_attrs['style']['dimensions']['minHeight'] ); } return ''; } /** * Get background color from block attributes. * * @param array $block_attrs Block attributes. * @param Rendering_Context $rendering_context Rendering context. * @return string Background color or empty string. */ private function get_background_color( array $block_attrs, Rendering_Context $rendering_context ): string { // Check for custom overlay color first (used as background color when no image). if ( ! empty( $block_attrs['customOverlayColor'] ) ) { $color = $block_attrs['customOverlayColor']; $sanitized_color = $this->validate_and_sanitize_color( $color ); if ( ! empty( $sanitized_color ) ) { return $sanitized_color; } } // Check for overlay color slug (used as background color when no image). if ( ! empty( $block_attrs['overlayColor'] ) ) { $translated_color = $rendering_context->translate_slug_to_color( $block_attrs['overlayColor'] ); $sanitized_color = $this->validate_and_sanitize_color( $translated_color ); if ( ! empty( $sanitized_color ) ) { return $sanitized_color; } } return ''; } /** * Validate and sanitize a color value, returning empty string for invalid colors. * * @param string $color The color value to validate and sanitize. * @return string Sanitized color or empty string if invalid. */ private function validate_and_sanitize_color( string $color ): string { $sanitized_color = Html_Processing_Helper::sanitize_color( $color ); // If sanitize_color returned the default fallback, check if the original was actually valid. if ( '#000000' === $sanitized_color && '#000000' !== $color ) { // The original color was invalid, so return empty string. return ''; } // The color is valid (either it was sanitized to something other than the default, // or it was specifically #000000 which is a valid color). return $sanitized_color; } /** * Build the cover content with background image or color. * * @param string $inner_content Inner content. * @return string Cover content HTML. */ private function build_cover_content( string $inner_content ): string { $cover_style = 'position: relative; display: inline-block; width: 100%; max-width: 100%;'; // Wrap inner content with padding. // Note: $inner_content is already rendered HTML from other blocks via render_block(), // so it should be properly escaped by the individual block renderers. $inner_wrapper_style = 'padding: 20px;'; $inner_wrapper_html = sprintf( '<div class="wp-block-cover__inner-container" style="%s">%s</div>', $inner_wrapper_style, $inner_content ); return sprintf( '<div class="wp-block-cover" style="%s">%s</div>', $cover_style, $inner_wrapper_html ); } } email-editor/src/Integrations/Core/Renderer/Blocks/class-buttons.php 0000777 00000003305 15252010675 0021554 0 ustar 00 <?php /** * This file is part of the WooCommerce Email Editor package * * @package Automattic\WooCommerce\EmailEditor */ declare( strict_types = 1 ); namespace Automattic\WooCommerce\EmailEditor\Integrations\Core\Renderer\Blocks; use Automattic\WooCommerce\EmailEditor\Engine\Renderer\ContentRenderer\Layout\Flex_Layout_Renderer; use Automattic\WooCommerce\EmailEditor\Engine\Renderer\ContentRenderer\Rendering_Context; /** * Renders a buttons block. */ class Buttons extends Abstract_Block_Renderer { /** * Provides the Flex_Layout_Renderer instance. * * @var Flex_Layout_Renderer */ private $flex_layout_renderer; /** * Buttons constructor. * * @param Flex_Layout_Renderer $flex_layout_renderer Flex layout renderer. */ public function __construct( Flex_Layout_Renderer $flex_layout_renderer ) { $this->flex_layout_renderer = $flex_layout_renderer; } /** * Renders the block content. * * @param string $block_content Block content. * @param array $parsed_block Parsed block. * @param Rendering_Context $rendering_context Rendering context. * @return string */ protected function render_content( $block_content, array $parsed_block, Rendering_Context $rendering_context ): string { // Ignore font size set on the buttons block. // We rely on TypographyPreprocessor to set the font size on the buttons. // Rendering font size on the wrapper causes unwanted whitespace below the buttons. if ( isset( $parsed_block['attrs']['style']['typography']['fontSize'] ) ) { unset( $parsed_block['attrs']['style']['typography']['fontSize'] ); } return $this->flex_layout_renderer->render_inner_blocks_in_layout( $parsed_block, $rendering_context ); } } email-editor/src/Integrations/Core/Renderer/Blocks/class-embed.php 0000777 00000043502 15252010675 0021135 0 ustar 00 <?php /** * This file is part of the WooCommerce Email Editor package * * @package Automattic\WooCommerce\EmailEditor */ declare( strict_types = 1 ); namespace Automattic\WooCommerce\EmailEditor\Integrations\Core\Renderer\Blocks; use Automattic\WooCommerce\EmailEditor\Engine\Renderer\ContentRenderer\Rendering_Context; use Automattic\WooCommerce\EmailEditor\Integrations\Core\Renderer\Blocks\Audio; use Automattic\WooCommerce\EmailEditor\Integrations\Core\Renderer\Blocks\Video; use Automattic\WooCommerce\EmailEditor\Integrations\Utils\Dom_Document_Helper; use Automattic\WooCommerce\EmailEditor\Integrations\Utils\Html_Processing_Helper; /** * Embed block renderer. * This renderer handles core/embed blocks, detecting audio and video provider embeds and rendering them appropriately. * * Audio providers: Spotify, SoundCloud, Pocket Casts, Mixcloud, ReverbNation - rendered as audio players. * Video providers: YouTube - rendered as video thumbnails with play buttons. */ class Embed extends Abstract_Block_Renderer { /** * Supported audio providers with their configuration. * * @var array */ private const AUDIO_PROVIDERS = array( 'pocket-casts' => array( 'domains' => array( 'pca.st' ), 'base_url' => 'https://pca.st/', ), 'spotify' => array( 'domains' => array( 'open.spotify.com' ), 'base_url' => 'https://open.spotify.com/', ), 'soundcloud' => array( 'domains' => array( 'soundcloud.com' ), 'base_url' => 'https://soundcloud.com/', ), 'mixcloud' => array( 'domains' => array( 'mixcloud.com' ), 'base_url' => 'https://www.mixcloud.com/', ), 'reverbnation' => array( 'domains' => array( 'reverbnation.com' ), 'base_url' => 'https://www.reverbnation.com/', ), ); /** * Supported video providers with their configuration. * * @var array */ private const VIDEO_PROVIDERS = array( 'youtube' => array( 'domains' => array( 'youtube.com', 'youtu.be' ), 'base_url' => 'https://www.youtube.com/', ), ); /** * Get all supported providers (audio and video). * * @return array All supported providers. */ private function get_all_supported_providers(): array { return array_merge( array_keys( self::AUDIO_PROVIDERS ), array_keys( self::VIDEO_PROVIDERS ) ); } /** * Get all provider configurations (audio and video). * * @return array All provider configurations. */ private function get_all_provider_configs(): array { return array_merge( self::AUDIO_PROVIDERS, self::VIDEO_PROVIDERS ); } /** * Detect provider from content by checking against known domains. * * @param string $content Content to check for provider domains. * @return string Provider name or empty string if not found. */ private function detect_provider_from_domains( string $content ): string { $all_providers = $this->get_all_provider_configs(); foreach ( $all_providers as $provider => $config ) { foreach ( $config['domains'] as $domain ) { if ( strpos( $content, $domain ) !== false ) { return $provider; } } } return ''; } /** * Validate URL using both filter_var and wp_http_validate_url. * * @param string $url URL to validate. * @return bool True if URL is valid. */ private function is_valid_url( string $url ): bool { return ! empty( $url ) && filter_var( $url, FILTER_VALIDATE_URL ) && wp_http_validate_url( $url ); } /** * Create fallback attributes for link rendering. * * @param string $url URL for the fallback. * @param string $label Label for the fallback. * @return array Fallback attributes. */ private function create_fallback_attributes( string $url, string $label ): array { return array( 'url' => $url, 'label' => $label, ); } /** * Renders the embed block. * * @param string $block_content Block content. * @param array $parsed_block Parsed block. * @param Rendering_Context $rendering_context Rendering context. * @return string */ public function render( string $block_content, array $parsed_block, Rendering_Context $rendering_context ): string { // Validate input parameters and required dependencies. if ( ! isset( $parsed_block['attrs'] ) || ! is_array( $parsed_block['attrs'] ) || ! class_exists( '\Automattic\WooCommerce\EmailEditor\Integrations\Utils\Table_Wrapper_Helper' ) ) { return ''; } $attr = $parsed_block['attrs']; // Check if this is a supported audio or video provider embed and has a valid URL. $provider = $this->get_supported_provider( $attr, $block_content ); if ( empty( $provider ) ) { // For non-supported embeds, try to render as a simple link fallback. return $this->render_link_fallback( $attr, $block_content, $parsed_block, $rendering_context ); } $url = $this->extract_provider_url( $attr, $block_content ); if ( empty( $url ) ) { // Provider was detected but URL extraction failed - provide graceful fallback. return $this->render_link_fallback( $attr, $block_content, $parsed_block, $rendering_context ); } // If we have a valid audio or video provider embed, proceed with normal rendering. return $this->render_content( $block_content, $parsed_block, $rendering_context ); } /** * Renders the embed block content. * * @param string $block_content Block content. * @param array $parsed_block Parsed block. * @param Rendering_Context $rendering_context Rendering context (required by parent contract but unused in this implementation). * @return string */ protected function render_content( string $block_content, array $parsed_block, Rendering_Context $rendering_context ): string { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable $attr = $parsed_block['attrs'] ?? array(); // Get provider and URL (validation already done in render method). $provider = $this->get_supported_provider( $attr, $block_content ); $url = $this->extract_provider_url( $attr, $block_content ); // Check if this is a video provider - render as video block. if ( $this->is_video_provider( $provider ) ) { return $this->render_video_embed( $url, $provider, $parsed_block, $rendering_context, $block_content ); } // For audio providers, use the original audio rendering logic. $label = $this->get_provider_label( $provider, $attr ); // Create a mock audio block structure to reuse the Audio renderer. $mock_audio_block = array( 'blockName' => 'core/audio', 'attrs' => array( 'src' => $url, 'label' => $label, ), 'innerHTML' => '<figure class="wp-block-audio"><audio controls src="' . esc_attr( $url ) . '"></audio></figure>', ); // Copy email attributes to the mock block. if ( isset( $parsed_block['email_attrs'] ) ) { $mock_audio_block['email_attrs'] = $parsed_block['email_attrs']; } // Use the Audio renderer to render the audio provider embed. $audio_renderer = new Audio(); $audio_result = $audio_renderer->render( $mock_audio_block['innerHTML'], $mock_audio_block, $rendering_context ); // If audio rendering fails, fall back to a simple link. if ( empty( $audio_result ) ) { $fallback_attr = $this->create_fallback_attributes( $url, $label ); return $this->render_link_fallback( $fallback_attr, $block_content, $parsed_block, $rendering_context ); } return $audio_result; } /** * Get supported audio or video provider from block attributes or content. * * @param array $attr Block attributes. * @param string $block_content Block content. * @return string Provider name or empty string if not supported. */ private function get_supported_provider( array $attr, string $block_content ): string { $all_supported_providers = $this->get_all_supported_providers(); // Check provider name slug. if ( isset( $attr['providerNameSlug'] ) && in_array( $attr['providerNameSlug'], $all_supported_providers, true ) ) { return $attr['providerNameSlug']; } // Check for supported domains in URL or content. $url = $attr['url'] ?? ''; $content_to_check = ! empty( $url ) ? $url : $block_content; // Use sophisticated domain detection logic. return $this->detect_provider_from_domains( $content_to_check ); } /** * Extract URL from block content using DOM parsing. * * @param string $block_content Block content HTML. * @return string Extracted URL or empty string. */ private function extract_url_from_content( string $block_content ): string { $dom_helper = new Dom_Document_Helper( $block_content ); // Find the wp-block-embed__wrapper div. $wrapper_element = $dom_helper->find_element( 'div' ); if ( $wrapper_element ) { // Check if this div has the correct class. $class_attr = $dom_helper->get_attribute_value( $wrapper_element, 'class' ); if ( strpos( $class_attr, 'wp-block-embed__wrapper' ) !== false ) { // Get the text content (URL) from the div. $url = trim( $wrapper_element->textContent ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase // Decode HTML entities and validate URL. $url = html_entity_decode( $url, ENT_QUOTES | ENT_HTML5, 'UTF-8' ); // Validate the extracted URL. if ( $this->is_valid_url( $url ) ) { return $url; } } } return ''; } /** * Extract provider URL from block attributes or content. * * @param array $attr Block attributes. * @param string $block_content Block content. * @return string Provider URL or empty string. */ private function extract_provider_url( array $attr, string $block_content ): string { // First, try to get URL from attributes. if ( ! empty( $attr['url'] ) ) { $url = $attr['url']; // Validate the URL from attributes. if ( $this->is_valid_url( $url ) ) { return $url; } return ''; } // If not in attributes, extract from block content. return $this->extract_url_from_content( $block_content ); } /** * Get appropriate label for the provider. * * @param string $provider Provider name. * @param array $attr Block attributes. * @return string Label for the provider. */ private function get_provider_label( string $provider, array $attr ): string { // Use custom label if provided. if ( ! empty( $attr['label'] ) ) { return $attr['label']; } // Get translated label for the provider. return $this->get_translated_provider_label( $provider ); } /** * Get translated label for a provider. * * @param string $provider Provider name. * @return string Translated label for the provider. */ private function get_translated_provider_label( string $provider ): string { switch ( $provider ) { case 'spotify': return __( 'Listen on Spotify', 'woocommerce' ); case 'soundcloud': return __( 'Listen on SoundCloud', 'woocommerce' ); case 'pocket-casts': return __( 'Listen on Pocket Casts', 'woocommerce' ); case 'mixcloud': return __( 'Listen on Mixcloud', 'woocommerce' ); case 'reverbnation': return __( 'Listen on ReverbNation', 'woocommerce' ); case 'youtube': return __( 'Watch on YouTube', 'woocommerce' ); default: return __( 'Listen to the audio', 'woocommerce' ); } } /** * Render a simple link fallback for non-supported embeds. * * @param array $attr Block attributes. * @param string $block_content Block content. * @param array $parsed_block Parsed block. * @param Rendering_Context $rendering_context Rendering context. * @return string Rendered link or empty string if no valid URL. */ private function render_link_fallback( array $attr, string $block_content, array $parsed_block, Rendering_Context $rendering_context ): string { // Try to get URL from attributes first. $url = $attr['url'] ?? ''; // If no URL in attributes, try to extract from block content. if ( empty( $url ) ) { // First try the standard wrapper div extraction. $url = $this->extract_url_from_content( $block_content ); // If still no URL, try to find any HTTP/HTTPS URL in the entire content. if ( empty( $url ) ) { $dom_helper = new Dom_Document_Helper( $block_content ); $body_element = $dom_helper->find_element( 'body' ); if ( $body_element ) { $text_content = $body_element->textContent; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase // Look for HTTP/HTTPS URLs in the text content. if ( preg_match( '/(?<![a-zA-Z0-9.-])https?:\/\/[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}[a-zA-Z0-9\/?=&%-]*(?![a-zA-Z0-9.-])/', $text_content, $matches ) ) { $url = $matches[0]; } } } } // If still no URL, try to use provider-specific base URL if we have a provider. if ( empty( $url ) && isset( $attr['providerNameSlug'] ) ) { $url = $this->get_provider_base_url( $attr['providerNameSlug'] ); } // Validate URL with both filter_var and wp_http_validate_url. if ( ! $this->is_valid_url( $url ) ) { return ''; } // Get link text - use custom label if provided, otherwise use provider label for base URLs or URL. if ( ! empty( $attr['label'] ) ) { $link_text = $attr['label']; } else { // Check if this is a provider base URL (like https://open.spotify.com/). $provider = $attr['providerNameSlug'] ?? ''; $base_url = $this->get_provider_base_url( $provider ); if ( ! empty( $base_url ) && $url === $base_url ) { // Use provider-specific label for base URLs. $link_text = $this->get_provider_label( $provider, $attr ); } else { // Use the URL itself for specific URLs. $link_text = $url; } } // Get color from email attributes or theme styles. $email_styles = $rendering_context->get_theme_styles(); $link_color = $parsed_block['email_attrs']['color'] ?? $email_styles['color']['text'] ?? '#0073aa'; // Sanitize color value to ensure it's a valid hex color or CSS variable. $link_color = Html_Processing_Helper::sanitize_color( $link_color ); // Create a simple link. $link_html = sprintf( '<a href="%s" target="_blank" rel="noopener nofollow" style="color: %s; text-decoration: underline;">%s</a>', esc_url( $url ), esc_attr( $link_color ), esc_html( $link_text ) ); // Wrap with spacer if we have email attributes. return $this->add_spacer( $link_html, $parsed_block['email_attrs'] ?? array() ); } /** * Get base URL for a provider when specific URL extraction fails. * * @param string $provider Provider name. * @return string Base URL for the provider or empty string. */ private function get_provider_base_url( string $provider ): string { $all_providers = $this->get_all_provider_configs(); return $all_providers[ $provider ]['base_url'] ?? ''; } /** * Check if a provider is a video provider. * * @param string $provider Provider name. * @return bool True if video provider. */ private function is_video_provider( string $provider ): bool { return array_key_exists( $provider, self::VIDEO_PROVIDERS ); } /** * Render a video embed using the Video renderer. * * @param string $url URL of the video. * @param string $provider Provider name. * @param array $parsed_block Parsed block. * @param Rendering_Context $rendering_context Rendering context. * @param string $block_content Original block content. * @return string Rendered video embed or fallback. */ private function render_video_embed( string $url, string $provider, array $parsed_block, Rendering_Context $rendering_context, string $block_content ): string { // Try to get video thumbnail URL. $poster_url = $this->get_video_thumbnail_url( $url, $provider ); // If no poster available, fall back to a simple link. if ( empty( $poster_url ) ) { $fallback_attr = $this->create_fallback_attributes( $url, $url ); return $this->render_link_fallback( $fallback_attr, $block_content, $parsed_block, $rendering_context ); } // Create a mock video block structure to reuse the Video renderer. $mock_video_block = array( 'blockName' => 'core/video', 'attrs' => array( 'poster' => $poster_url, ), 'innerHTML' => '<figure class="wp-block-video wp-block-embed is-type-video is-provider-' . esc_attr( $provider ) . '"><div class="wp-block-embed__wrapper">' . esc_url( $url ) . '</div></figure>', ); // Copy email attributes to the mock block. if ( isset( $parsed_block['email_attrs'] ) ) { $mock_video_block['email_attrs'] = $parsed_block['email_attrs']; } // Use the Video renderer to render the video provider embed. $video_renderer = new Video(); $video_result = $video_renderer->render( $mock_video_block['innerHTML'], $mock_video_block, $rendering_context ); // If video rendering fails, fall back to a simple link. if ( empty( $video_result ) ) { $fallback_attr = $this->create_fallback_attributes( $url, $url ); return $this->render_link_fallback( $fallback_attr, $block_content, $parsed_block, $rendering_context ); } return $video_result; } /** * Get video thumbnail URL for supported providers. * * @param string $url Video URL. * @param string $provider Provider name. * @return string Thumbnail URL or empty string. */ private function get_video_thumbnail_url( string $url, string $provider ): string { // Currently only YouTube supports thumbnail extraction. if ( 'youtube' === $provider ) { return $this->get_youtube_thumbnail( $url ); } // For other providers, we don't have thumbnail extraction implemented. // Return empty to trigger link fallback. return ''; } /** * Extract YouTube video thumbnail URL. * * @param string $url YouTube video URL. * @return string Thumbnail URL or empty string. */ private function get_youtube_thumbnail( string $url ): string { // Extract video ID from various YouTube URL formats. $video_id = ''; if ( preg_match( '/(?:youtube\.com\/watch\?v=|youtu\.be\/)([a-zA-Z0-9_-]+)/', $url, $matches ) ) { $video_id = $matches[1]; } if ( empty( $video_id ) ) { return ''; } // Return YouTube thumbnail URL. // Using 0.jpg format as shown in the example. return 'https://img.youtube.com/vi/' . $video_id . '/0.jpg'; } } email-editor/src/Integrations/Core/Renderer/Blocks/class-list-block.php 0000777 00000004340 15252010675 0022121 0 ustar 00 <?php /** * This file is part of the WooCommerce Email Editor package. * * @package Automattic\WooCommerce\EmailEditor */ declare( strict_types = 1 ); namespace Automattic\WooCommerce\EmailEditor\Integrations\Core\Renderer\Blocks; use Automattic\WooCommerce\EmailEditor\Engine\Renderer\ContentRenderer\Rendering_Context; use Automattic\WooCommerce\EmailEditor\Integrations\Utils\Styles_Helper; /** * Renders a list block. * We have to avoid using keyword `List` */ class List_Block extends Abstract_Block_Renderer { /** * Renders the block content * * @param string $block_content Block content. * @param array $parsed_block Parsed block. * @param Rendering_Context $rendering_context Rendering context. * @return string */ protected function render_content( string $block_content, array $parsed_block, Rendering_Context $rendering_context ): string { $html = new \WP_HTML_Tag_Processor( $block_content ); $tag_name = ( $parsed_block['attrs']['ordered'] ?? false ) ? 'ol' : 'ul'; if ( $html->next_tag( array( 'tag_name' => $tag_name ) ) ) { /** @var string $styles */ // phpcs:ignore Generic.Commenting.DocComment.MissingShort -- used for phpstan $styles = $html->get_attribute( 'style' ) ?? ''; $styles = Styles_Helper::parse_styles_to_array( $styles ); // Font size. if ( isset( $parsed_block['email_attrs']['font-size'] ) ) { $styles['font-size'] = $parsed_block['email_attrs']['font-size']; } else { // Use font-size from email theme when those properties are not set. $theme_data = $rendering_context->get_theme_json()->get_data(); $styles['font-size'] = $theme_data['styles']['typography']['fontSize']; } $html->set_attribute( 'style', esc_attr( \WP_Style_Engine::compile_css( $styles, '' ) ) ); $block_content = $html->get_updated_html(); } $wrapper_style = \WP_Style_Engine::compile_css( array( 'margin-top' => $parsed_block['email_attrs']['margin-top'] ?? '0px', ), '' ); // \WP_HTML_Tag_Processor escapes the content, so we have to replace it back $block_content = str_replace( ''', "'", $block_content ); return sprintf( '<div style="%1$s">%2$s</div>', esc_attr( $wrapper_style ), $block_content ); } } email-editor/src/Integrations/Core/Renderer/Blocks/class-post-content.php 0000777 00000006572 15252010675 0022524 0 ustar 00 <?php /** * This file is part of the WooCommerce Email Editor package * * @package Automattic\WooCommerce\EmailEditor */ declare( strict_types = 1 ); namespace Automattic\WooCommerce\EmailEditor\Integrations\Core\Renderer\Blocks; /** * Stateless renderer for core/post-content block. * * This renderer replaces WordPress's default render_block_core_post_content() * which uses a static $seen_ids array that causes issues when rendering multiple * emails in a single request (e.g., MailPoet batch processing). * * Unlike other block renderers, this class does NOT extend Abstract_Block_Renderer * because it needs to directly replace WordPress's render_callback with a method * that matches the exact signature expected by WordPress. */ class Post_Content { /** * Stateless render callback for core/post-content block. * * This implementation avoids using get_the_content() which relies on * global query state, and instead directly accesses post content * and applies the_content filter for processing. * * Key differences from WordPress's implementation: * - No static $seen_ids array (allows multiple renders in same request) * - Uses direct post content access instead of get_the_content() * - Properly backs up and restores global state * * IMPORTANT: This method is only set as the render_callback during email rendering. * Outside of email rendering, the original callback is restored, so this method * will never be called in non-email contexts. * * @param array $attributes Block attributes. * @param string $content Block content. * @param \WP_Block $block Block instance. * @return string Rendered post content HTML. */ public function render_stateless( $attributes, $content, $block ): string { // This method is only called during email rendering, so we always use stateless logic. $post_id = $block->context['postId'] ?? null; if ( ! $post_id ) { return ''; } $email_post = get_post( $post_id ); if ( ! $email_post || empty( $email_post->post_content ) ) { return ''; } // Backup global state. global $post, $wp_query; $backup_post = $post; $backup_query = $wp_query; // Set up global state for block rendering. // This ensures that blocks which depend on global $post work correctly. $post = $email_post; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited // Create a query specifically for this post to ensure proper context. $wp_query = new \WP_Query( array( 'p' => $post_id ) ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited // Get raw post content and apply the_content filter. // The the_content filter processes blocks, shortcodes, etc. // We don't use get_the_content() to avoid issues with loop state. $post_content = $email_post->post_content; // Check for nextpage to display page links for paginated posts. if ( has_block( 'core/nextpage', $email_post ) ) { $post_content .= wp_link_pages( array( 'echo' => 0 ) ); } // Apply the_content filter to process blocks. $post_content = apply_filters( 'the_content', str_replace( ']]>', ']]>', $post_content ) ); // Restore global state. $post = $backup_post; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited $wp_query = $backup_query; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited if ( empty( $post_content ) ) { return ''; } return $post_content; } } email-editor/src/Integrations/Core/Renderer/Blocks/icons/x/x-white.png 0000777 00000000752 15252010675 0021722 0 ustar 00 �PNG IHDR ` ` �F� PLTEGpL�������������1 tRNS ��@G'�� �IDATx��Ѫ�0�n��,\�R�x"�Ї��bs(�l��oggg�~�y������7ͫ|��ᮁ�Ӣ�r�9�Z��_C����OO�d]?!5�j������'�q�l+(!��RҺ�FE@ �zHB ����]RPB�*!q����U+h�����$�����@'A=���!AP���@CP �Η�7)| ��C�@]���@�C*8�#!���g��O读�KH��Cj� HQ���@�� I��3)���&H�`� � *����a!q缂4��O*v�+��D�� ��?c� �~4�~=��h-�� 9A iHA�(h�1�V�]��fHu\ ��@��א(hʼh��]���3�����OԿ���2�7�vvvv�Y���U^ IEND�B`� email-editor/src/Integrations/Core/Renderer/Blocks/icons/x/x-brand.png 0000777 00000000752 15252010675 0021670 0 ustar 00 �PNG IHDR ` ` �F� PLTEGpL Ӭ� tRNS ��@G'�� �IDATx��Ѫ�0�n��,\�R�x"�Ї��bs(�l��oggg�~�y������7ͫ|��ᮁ�Ӣ�r�9�Z��_C����OO�d]?!5�j������'�q�l+(!��RҺ�FE@ �zHB ����]RPB�*!q����U+h�����$�����@'A=���!AP���@CP �Η�7)| ��C�@]���@�C*8�#!���g��O读�KH��Cj� HQ���@�� I��3)���&H�`� � *����a!q缂4��O*v�+��D�� ��?c� �~4�~=��h-�� 9A iHA�(h�1�V�]��fHu\ ��@��א(hʼh��]���3�����OԿ���2�7�vvvv�Y���U^ IEND�B`� email-editor/src/Integrations/Core/Renderer/Blocks/icons/vimeo/vimeo-brand.png 0000777 00000000745 15252010675 0023412 0 ustar 00 �PNG IHDR ` ` �F� PLTEGpL��������ˋ� tRNS �@��� �IDATx��KnC1MJ�?s�n�$�즅g�|�"�.���`WU�(i��$aY?��}Ʋ'��C�7vT�g=��:���s<<B5��6�Bx n+,|B���/ �hmb�lm �>���A� �������'�G-�m �?<o�d�p %4xrz��5�{�6���w^����3���@��T�|胁p�pn� �w�dpF50 eӌ�8#�� gT� ���+2p<