mirror of
https://github.com/Combodo/iTop.git
synced 2026-02-13 07:24:13 +01:00
Update pelago/emogrifier bundle (^6.0.0)
>> files omitted
This commit is contained in:
committed by
bdalsass
parent
f6c50733fc
commit
742ef2b23b
229
lib/firebase/php-jwt/src/CachedKeySet.php
Normal file
229
lib/firebase/php-jwt/src/CachedKeySet.php
Normal file
@@ -0,0 +1,229 @@
|
||||
<?php
|
||||
|
||||
namespace Firebase\JWT;
|
||||
|
||||
use ArrayAccess;
|
||||
use LogicException;
|
||||
use OutOfBoundsException;
|
||||
use Psr\Cache\CacheItemInterface;
|
||||
use Psr\Cache\CacheItemPoolInterface;
|
||||
use Psr\Http\Client\ClientInterface;
|
||||
use Psr\Http\Message\RequestFactoryInterface;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* @implements ArrayAccess<string, Key>
|
||||
*/
|
||||
class CachedKeySet implements ArrayAccess
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $jwksUri;
|
||||
/**
|
||||
* @var ClientInterface
|
||||
*/
|
||||
private $httpClient;
|
||||
/**
|
||||
* @var RequestFactoryInterface
|
||||
*/
|
||||
private $httpFactory;
|
||||
/**
|
||||
* @var CacheItemPoolInterface
|
||||
*/
|
||||
private $cache;
|
||||
/**
|
||||
* @var ?int
|
||||
*/
|
||||
private $expiresAfter;
|
||||
/**
|
||||
* @var ?CacheItemInterface
|
||||
*/
|
||||
private $cacheItem;
|
||||
/**
|
||||
* @var array<string, Key>
|
||||
*/
|
||||
private $keySet;
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $cacheKey;
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $cacheKeyPrefix = 'jwks';
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $maxKeyLength = 64;
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $rateLimit;
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $rateLimitCacheKey;
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $maxCallsPerMinute = 10;
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
private $defaultAlg;
|
||||
|
||||
public function __construct(
|
||||
string $jwksUri,
|
||||
ClientInterface $httpClient,
|
||||
RequestFactoryInterface $httpFactory,
|
||||
CacheItemPoolInterface $cache,
|
||||
int $expiresAfter = null,
|
||||
bool $rateLimit = false,
|
||||
string $defaultAlg = null
|
||||
) {
|
||||
$this->jwksUri = $jwksUri;
|
||||
$this->httpClient = $httpClient;
|
||||
$this->httpFactory = $httpFactory;
|
||||
$this->cache = $cache;
|
||||
$this->expiresAfter = $expiresAfter;
|
||||
$this->rateLimit = $rateLimit;
|
||||
$this->defaultAlg = $defaultAlg;
|
||||
$this->setCacheKeys();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $keyId
|
||||
* @return Key
|
||||
*/
|
||||
public function offsetGet($keyId): Key
|
||||
{
|
||||
if (!$this->keyIdExists($keyId)) {
|
||||
throw new OutOfBoundsException('Key ID not found');
|
||||
}
|
||||
return $this->keySet[$keyId];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $keyId
|
||||
* @return bool
|
||||
*/
|
||||
public function offsetExists($keyId): bool
|
||||
{
|
||||
return $this->keyIdExists($keyId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $offset
|
||||
* @param Key $value
|
||||
*/
|
||||
public function offsetSet($offset, $value): void
|
||||
{
|
||||
throw new LogicException('Method not implemented');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $offset
|
||||
*/
|
||||
public function offsetUnset($offset): void
|
||||
{
|
||||
throw new LogicException('Method not implemented');
|
||||
}
|
||||
|
||||
private function keyIdExists(string $keyId): bool
|
||||
{
|
||||
if (null === $this->keySet) {
|
||||
$item = $this->getCacheItem();
|
||||
// Try to load keys from cache
|
||||
if ($item->isHit()) {
|
||||
// item found! Return it
|
||||
$jwks = $item->get();
|
||||
$this->keySet = JWK::parseKeySet(json_decode($jwks, true), $this->defaultAlg);
|
||||
}
|
||||
}
|
||||
|
||||
if (!isset($this->keySet[$keyId])) {
|
||||
if ($this->rateLimitExceeded()) {
|
||||
return false;
|
||||
}
|
||||
$request = $this->httpFactory->createRequest('get', $this->jwksUri);
|
||||
$jwksResponse = $this->httpClient->sendRequest($request);
|
||||
$jwks = (string) $jwksResponse->getBody();
|
||||
$this->keySet = JWK::parseKeySet(json_decode($jwks, true), $this->defaultAlg);
|
||||
|
||||
if (!isset($this->keySet[$keyId])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$item = $this->getCacheItem();
|
||||
$item->set($jwks);
|
||||
if ($this->expiresAfter) {
|
||||
$item->expiresAfter($this->expiresAfter);
|
||||
}
|
||||
$this->cache->save($item);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function rateLimitExceeded(): bool
|
||||
{
|
||||
if (!$this->rateLimit) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$cacheItem = $this->cache->getItem($this->rateLimitCacheKey);
|
||||
if (!$cacheItem->isHit()) {
|
||||
$cacheItem->expiresAfter(1); // # of calls are cached each minute
|
||||
}
|
||||
|
||||
$callsPerMinute = (int) $cacheItem->get();
|
||||
if (++$callsPerMinute > $this->maxCallsPerMinute) {
|
||||
return true;
|
||||
}
|
||||
$cacheItem->set($callsPerMinute);
|
||||
$this->cache->save($cacheItem);
|
||||
return false;
|
||||
}
|
||||
|
||||
private function getCacheItem(): CacheItemInterface
|
||||
{
|
||||
if (\is_null($this->cacheItem)) {
|
||||
$this->cacheItem = $this->cache->getItem($this->cacheKey);
|
||||
}
|
||||
|
||||
return $this->cacheItem;
|
||||
}
|
||||
|
||||
private function setCacheKeys(): void
|
||||
{
|
||||
if (empty($this->jwksUri)) {
|
||||
throw new RuntimeException('JWKS URI is empty');
|
||||
}
|
||||
|
||||
// ensure we do not have illegal characters
|
||||
$key = preg_replace('|[^a-zA-Z0-9_\.!]|', '', $this->jwksUri);
|
||||
|
||||
// add prefix
|
||||
$key = $this->cacheKeyPrefix . $key;
|
||||
|
||||
// Hash keys if they exceed $maxKeyLength of 64
|
||||
if (\strlen($key) > $this->maxKeyLength) {
|
||||
$key = substr(hash('sha256', $key), 0, $this->maxKeyLength);
|
||||
}
|
||||
|
||||
$this->cacheKey = $key;
|
||||
|
||||
if ($this->rateLimit) {
|
||||
// add prefix
|
||||
$rateLimitKey = $this->cacheKeyPrefix . 'ratelimit' . $key;
|
||||
|
||||
// Hash keys if they exceed $maxKeyLength of 64
|
||||
if (\strlen($rateLimitKey) > $this->maxKeyLength) {
|
||||
$rateLimitKey = substr(hash('sha256', $rateLimitKey), 0, $this->maxKeyLength);
|
||||
}
|
||||
|
||||
$this->rateLimitCacheKey = $rateLimitKey;
|
||||
}
|
||||
}
|
||||
}
|
||||
23
lib/pelago/emogrifier/phpunit.xml
Normal file
23
lib/pelago/emogrifier/phpunit.xml
Normal file
@@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/8.5/phpunit.xsd"
|
||||
bootstrap="vendor/autoload.php"
|
||||
colors="true"
|
||||
executionOrder="depends,defects"
|
||||
forceCoversAnnotation="true"
|
||||
beStrictAboutCoversAnnotation="true"
|
||||
beStrictAboutOutputDuringTests="true"
|
||||
beStrictAboutTodoAnnotatedTests="true"
|
||||
verbose="true">
|
||||
<testsuites>
|
||||
<testsuite name="default">
|
||||
<directory>tests</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
|
||||
<filter>
|
||||
<whitelist processUncoveredFilesFromWhitelist="true">
|
||||
<directory suffix=".php">src</directory>
|
||||
</whitelist>
|
||||
</filter>
|
||||
</phpunit>
|
||||
87
lib/pelago/emogrifier/src/Caching/SimpleStringCache.php
Normal file
87
lib/pelago/emogrifier/src/Caching/SimpleStringCache.php
Normal file
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace 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
|
||||
*/
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
187
lib/pelago/emogrifier/src/Css/CssDocument.php
Normal file
187
lib/pelago/emogrifier/src/Css/CssDocument.php
Normal file
@@ -0,0 +1,187 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pelago\Emogrifier\Css;
|
||||
|
||||
use Sabberworm\CSS\CSSList\AtRuleBlockList as CssAtRuleBlockList;
|
||||
use Sabberworm\CSS\CSSList\Document as SabberwormCssDocument;
|
||||
use Sabberworm\CSS\Parser as CssParser;
|
||||
use Sabberworm\CSS\Property\AtRule as CssAtRule;
|
||||
use Sabberworm\CSS\Property\Charset as CssCharset;
|
||||
use Sabberworm\CSS\Property\Import as CssImport;
|
||||
use Sabberworm\CSS\Renderable as CssRenderable;
|
||||
use Sabberworm\CSS\RuleSet\DeclarationBlock as CssDeclarationBlock;
|
||||
use Sabberworm\CSS\RuleSet\RuleSet as CssRuleSet;
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
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
|
||||
*/
|
||||
public function __construct(string $css)
|
||||
{
|
||||
$cssParser = new CssParser($css);
|
||||
/** @var SabberwormCssDocument $sabberwormCssDocument */
|
||||
$sabberwormCssDocument = $cssParser->parse();
|
||||
$this->sabberwormCssDocument = $sabberwormCssDocument;
|
||||
}
|
||||
|
||||
/**
|
||||
* Collates the media query, selectors and declarations for individual rules from the parsed CSS, in order.
|
||||
*
|
||||
* @param array<array-key, string> $allowedMediaTypes
|
||||
*
|
||||
* @return array<int, 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;
|
||||
/** @var array<int, CssRenderable> $cssContents */
|
||||
$cssContents = $this->sabberwormCssDocument->getContents();
|
||||
$atRules = \array_filter($cssContents, [$this, 'isValidAtRuleToRender']);
|
||||
|
||||
if ($atRules === []) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$atRulesDocument = new SabberwormCssDocument();
|
||||
$atRulesDocument->setContents($atRules);
|
||||
|
||||
/** @var string $renderedRules */
|
||||
$renderedRules = $atRulesDocument->render();
|
||||
return $renderedRules;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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') {
|
||||
/** @var string $mediaQueryList */
|
||||
$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 = \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;
|
||||
}
|
||||
}
|
||||
83
lib/pelago/emogrifier/src/Css/StyleRule.php
Normal file
83
lib/pelago/emogrifier/src/Css/StyleRule.php
Normal file
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pelago\Emogrifier\Css;
|
||||
|
||||
use Sabberworm\CSS\Property\Selector;
|
||||
use Sabberworm\CSS\RuleSet\DeclarationBlock;
|
||||
|
||||
/**
|
||||
* This class represents a CSS style rule, including selectors, a declaration block, and an optional containing at-rule.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
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() !== '';
|
||||
}
|
||||
}
|
||||
1196
lib/pelago/emogrifier/src/CssInliner.php
Normal file
1196
lib/pelago/emogrifier/src/CssInliner.php
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,472 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pelago\Emogrifier\HtmlProcessor;
|
||||
|
||||
/**
|
||||
* Base class for HTML processor that e.g., can remove, add or modify nodes or attributes.
|
||||
*
|
||||
* The "vanilla" subclass is the HtmlNormalizer.
|
||||
*
|
||||
* @psalm-consistent-constructor
|
||||
*/
|
||||
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 \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 \preg_replace('%</' . self::PHP_UNRECOGNIZED_VOID_TAGNAME_MATCHER . '>%', '', $html);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 = true;
|
||||
$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 \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 = \preg_match('/<head[\\s>]/i', $html);
|
||||
$hasHtmlTag = \stripos($html, '<html') !== false;
|
||||
|
||||
if ($hasHeadTag) {
|
||||
$reworkedHtml = \preg_replace(
|
||||
'/<head(?=[\\s>])([^>]*+)>/i',
|
||||
'<head$1>' . self::CONTENT_TYPE_META_TAG,
|
||||
$html
|
||||
);
|
||||
} elseif ($hasHtmlTag) {
|
||||
$reworkedHtml = \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
|
||||
{
|
||||
\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
|
||||
{
|
||||
$headEndTagMatchCount
|
||||
= \preg_match('%<(?!' . self::TAGNAME_ALLOWED_BEFORE_BODY_MATCHER . '[\\s/>])\\w|</head>%i', $html);
|
||||
if (\is_int($headEndTagMatchCount) && $headEndTagMatchCount > 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
|
||||
{
|
||||
$result = \preg_replace(self::HTML_COMMENT_PATTERN, '', $html);
|
||||
if (!\is_string($result)) {
|
||||
throw new \RuntimeException('Internal PCRE error', 1616521475);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
{
|
||||
$result = \preg_replace(self::HTML_TEMPLATE_ELEMENT_PATTERN, '', $html);
|
||||
if (!\is_string($result)) {
|
||||
throw new \RuntimeException('Internal PCRE error', 1616519652);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 \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;
|
||||
}
|
||||
|
||||
$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);
|
||||
}
|
||||
$htmlElement->appendChild($this->getDomDocument()->createElement('body'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,303 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pelago\Emogrifier\HtmlProcessor;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
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 a whitelist
|
||||
* 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'],
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* @var array<string, array<string, string>>
|
||||
*/
|
||||
private static $parsedCssCache = [];
|
||||
|
||||
/**
|
||||
* Maps the CSS from the style nodes to visual HTML attributes.
|
||||
*
|
||||
* @return self fluent interface
|
||||
*/
|
||||
public function convertCssToVisualAttributes(): self
|
||||
{
|
||||
/** @var \DOMElement $node */
|
||||
foreach ($this->getAllNodesWithStyleAttribute() as $node) {
|
||||
$inlineStyleDeclarations = $this->parseCssDeclarationsBlock($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]');
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 $cssDeclarationsBlock the CSS declarations block without the curly braces, may be empty
|
||||
*
|
||||
* @return array<string, string>
|
||||
* the CSS declarations with the property names as array keys and the property values as array values
|
||||
*/
|
||||
private function parseCssDeclarationsBlock(string $cssDeclarationsBlock): array
|
||||
{
|
||||
if (isset(self::$parsedCssCache[$cssDeclarationsBlock])) {
|
||||
return self::$parsedCssCache[$cssDeclarationsBlock];
|
||||
}
|
||||
|
||||
$properties = [];
|
||||
foreach (\preg_split('/;(?!base64|charset)/', $cssDeclarationsBlock) as $declaration) {
|
||||
/** @var array<int, string> $matches */
|
||||
$matches = [];
|
||||
if (!\preg_match('/^([A-Za-z\\-]+)\\s*:\\s*(.+)$/s', \trim($declaration), $matches)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$propertyName = \strtolower($matches[1]);
|
||||
$propertyValue = $matches[2];
|
||||
$properties[$propertyName] = $propertyValue;
|
||||
}
|
||||
self::$parsedCssCache[$cssDeclarationsBlock] = $properties;
|
||||
|
||||
return $properties;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
{
|
||||
// only parse values in px and %, but not values like "auto"
|
||||
if (!\preg_match('/^(\\d+)(\\.(\\d+))?(px|%)$/', $value)) {
|
||||
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
|
||||
{
|
||||
/** @var array<int, string> $values */
|
||||
$values = \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;
|
||||
}
|
||||
}
|
||||
16
lib/pelago/emogrifier/src/HtmlProcessor/HtmlNormalizer.php
Normal file
16
lib/pelago/emogrifier/src/HtmlProcessor/HtmlNormalizer.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace 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
|
||||
*/
|
||||
class HtmlNormalizer extends AbstractHtmlProcessor
|
||||
{
|
||||
}
|
||||
137
lib/pelago/emogrifier/src/HtmlProcessor/HtmlPruner.php
Normal file
137
lib/pelago/emogrifier/src/HtmlProcessor/HtmlPruner.php
Normal file
@@ -0,0 +1,137 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pelago\Emogrifier\HtmlProcessor;
|
||||
|
||||
use Pelago\Emogrifier\CssInliner;
|
||||
use Pelago\Emogrifier\Utilities\ArrayIntersector;
|
||||
|
||||
/**
|
||||
* This class can remove things from HTML.
|
||||
*/
|
||||
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 self fluent interface
|
||||
*/
|
||||
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 self fluent interface
|
||||
*/
|
||||
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);
|
||||
|
||||
/** @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 self fluent interface
|
||||
*
|
||||
* @throws \BadMethodCallException if `inlineCss` has not first been called on `$cssInliner`
|
||||
*/
|
||||
public function removeRedundantClassesAfterCssInlined(CssInliner $cssInliner): self
|
||||
{
|
||||
$classesToKeepAsKeys = [];
|
||||
foreach ($cssInliner->getMatchingUninlinableSelectors() as $selector) {
|
||||
\preg_match_all('/\\.(-?+[_a-zA-Z][\\w\\-]*+)/', $selector, $matches);
|
||||
$classesToKeepAsKeys += \array_fill_keys($matches[1], true);
|
||||
}
|
||||
|
||||
$this->removeRedundantClasses(\array_keys($classesToKeepAsKeys));
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
57
lib/pelago/emogrifier/src/Utilities/ArrayIntersector.php
Normal file
57
lib/pelago/emogrifier/src/Utilities/ArrayIntersector.php
Normal file
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace 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
|
||||
*/
|
||||
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);
|
||||
}
|
||||
}
|
||||
181
lib/pelago/emogrifier/src/Utilities/CssConcatenator.php
Normal file
181
lib/pelago/emogrifier/src/Utilities/CssConcatenator.php
Normal file
@@ -0,0 +1,181 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace 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
|
||||
*/
|
||||
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 . '}';
|
||||
}
|
||||
}
|
||||
241
lib/sabberworm/php-css-parser/CHANGELOG.md
Normal file
241
lib/sabberworm/php-css-parser/CHANGELOG.md
Normal file
@@ -0,0 +1,241 @@
|
||||
# Revision History
|
||||
|
||||
## 8.4.0
|
||||
|
||||
### Features
|
||||
|
||||
* Support for PHP 8.x
|
||||
* PHPDoc annotations
|
||||
* Allow usage of CSS variables inside color functions (by parsing them as regular functions)
|
||||
* Use PSR-12 code style
|
||||
* *No deprecations*
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Improved handling of whitespace in `calc()`
|
||||
* Fix parsing units whose prefix is also a valid unit, like `vmin`
|
||||
* Allow passing an object to `CSSList#replace`
|
||||
* Fix PHP 7.3 warnings
|
||||
* Correctly parse keyframes with `%`
|
||||
* Don’t convert large numbers to scientific notation
|
||||
* Allow a file to end after an `@import`
|
||||
* Preserve case of CSS variables as specced
|
||||
* Allow identifiers to use escapes the same way as strings
|
||||
* No longer use `eval` for the comparison in `getSelectorsBySpecificity`, in case it gets passed untrusted input (CVE-2020-13756). Also fixed in 8.3.1, 8.2.1, 8.1.1, 8.0.1, 7.0.4, 6.0.2, 5.2.1, 5.1.3, 5.0.9, 4.0.1, 3.0.1, 2.0.1, 1.0.1.
|
||||
* Prevent an infinite loop when parsing invalid grid line names
|
||||
* Remove invalid unit `vm`
|
||||
* Retain rule order after expanding shorthands
|
||||
|
||||
### Backwards-incompatible changes
|
||||
|
||||
* PHP ≥ 5.6 is now required
|
||||
* HHVM compatibility target dropped
|
||||
|
||||
## 8.3.0 (2019-02-22)
|
||||
|
||||
* Refactor parsing logic to mostly reside in the class files whose data structure is to be parsed (this should eventually allow us to unit-test specific parts of the parsing logic individually).
|
||||
* Fix error in parsing `calc` expessions when the first operand is a negative number, thanks to @raxbg.
|
||||
* Support parsing CSS4 colors in hex notation with alpha values, thanks to @raxbg.
|
||||
* Swallow more errors in lenient mode, thanks to @raxbg.
|
||||
* Allow specifying arbitrary strings to output before and after declaration blocks, thanks to @westonruter.
|
||||
* *No backwards-incompatible changes*
|
||||
* *No deprecations*
|
||||
|
||||
## 8.2.0 (2018-07-13)
|
||||
|
||||
* Support parsing `calc()`, thanks to @raxbg.
|
||||
* Support parsing grid-lines, again thanks to @raxbg.
|
||||
* Support parsing legacy IE filters (`progid:`) in lenient mode, thanks to @FMCorz
|
||||
* Performance improvements parsing large files, again thanks to @FMCorz
|
||||
* *No backwards-incompatible changes*
|
||||
* *No deprecations*
|
||||
|
||||
## 8.1.0 (2016-07-19)
|
||||
|
||||
* Comments are no longer silently ignored but stored with the object with which they appear (no render support, though). Thanks to @FMCorz.
|
||||
* The IE hacks using `\0` and `\9` can now be parsed (and rendered) in lenient mode. Thanks (again) to @FMCorz.
|
||||
* Media queries with or without spaces before the query are parsed. Still no *real* parsing support, though. Sorry…
|
||||
* PHPUnit is now listed as a dev-dependency in composer.json.
|
||||
* *No backwards-incompatible changes*
|
||||
* *No deprecations*
|
||||
|
||||
## 8.0.0 (2016-06-30)
|
||||
|
||||
* Store source CSS line numbers in tokens and parsing exceptions.
|
||||
* *No deprecations*
|
||||
|
||||
### Backwards-incompatible changes
|
||||
|
||||
* Unrecoverable parser errors throw an exception of type `Sabberworm\CSS\Parsing\SourceException` instead of `\Exception`.
|
||||
|
||||
## 7.0.3 (2016-04-27)
|
||||
|
||||
* Fixed parsing empty CSS when multibyte is off
|
||||
* *No backwards-incompatible changes*
|
||||
* *No deprecations*
|
||||
|
||||
## 7.0.2 (2016-02-11)
|
||||
|
||||
* 150 time performance boost thanks to @[ossinkine](https://github.com/ossinkine)
|
||||
* *No backwards-incompatible changes*
|
||||
* *No deprecations*
|
||||
|
||||
## 7.0.1 (2015-12-25)
|
||||
|
||||
* No more suppressed `E_NOTICE`
|
||||
* *No backwards-incompatible changes*
|
||||
* *No deprecations*
|
||||
|
||||
## 7.0.0 (2015-08-24)
|
||||
|
||||
* Compatibility with PHP 7. Well timed, eh?
|
||||
* *No deprecations*
|
||||
|
||||
### Backwards-incompatible changes
|
||||
|
||||
* The `Sabberworm\CSS\Value\String` class has been renamed to `Sabberworm\CSS\Value\CSSString`.
|
||||
|
||||
## 6.0.1 (2015-08-24)
|
||||
|
||||
* Remove some declarations in interfaces incompatible with PHP 5.3 (< 5.3.9)
|
||||
* *No deprecations*
|
||||
|
||||
## 6.0.0 (2014-07-03)
|
||||
|
||||
* Format output using Sabberworm\CSS\OutputFormat
|
||||
* *No backwards-incompatible changes*
|
||||
|
||||
### Deprecations
|
||||
|
||||
* The parse() method replaces __toString with an optional argument (instance of the OutputFormat class)
|
||||
|
||||
## 5.2.0 (2014-06-30)
|
||||
|
||||
* Support removing a selector from a declaration block using `$oBlock->removeSelector($mSelector)`
|
||||
* Introduce a specialized exception (Sabberworm\CSS\Parsing\OuputException) for exceptions during output rendering
|
||||
|
||||
* *No deprecations*
|
||||
|
||||
#### Backwards-incompatible changes
|
||||
|
||||
* Outputting a declaration block that has no selectors throws an OuputException instead of outputting an invalid ` {…}` into the CSS document.
|
||||
|
||||
## 5.1.2 (2013-10-30)
|
||||
|
||||
* Remove the use of consumeUntil in comment parsing. This makes it possible to parse comments such as `/** Perfectly valid **/`
|
||||
* Add fr relative size unit
|
||||
* Fix some issues with HHVM
|
||||
* *No backwards-incompatible changes*
|
||||
* *No deprecations*
|
||||
|
||||
## 5.1.1 (2013-10-28)
|
||||
|
||||
* Updated CHANGELOG.md to reflect changes since 5.0.4
|
||||
* *No backwards-incompatible changes*
|
||||
* *No deprecations*
|
||||
|
||||
## 5.1.0 (2013-10-24)
|
||||
|
||||
* Performance enhancements by Michael M Slusarz
|
||||
* More rescue entry points for lenient parsing (unexpected tokens between declaration blocks and unclosed comments)
|
||||
* *No backwards-incompatible changes*
|
||||
* *No deprecations*
|
||||
|
||||
## 5.0.8 (2013-08-15)
|
||||
|
||||
* Make default settings’ multibyte parsing option dependent on whether or not the mbstring extension is actually installed.
|
||||
* *No backwards-incompatible changes*
|
||||
* *No deprecations*
|
||||
|
||||
## 5.0.7 (2013-08-04)
|
||||
|
||||
* Fix broken decimal point output optimization
|
||||
* *No backwards-incompatible changes*
|
||||
* *No deprecations*
|
||||
|
||||
## 5.0.6 (2013-05-31)
|
||||
|
||||
* Fix broken unit test
|
||||
* *No backwards-incompatible changes*
|
||||
* *No deprecations*
|
||||
|
||||
## 5.0.5 (2013-04-17)
|
||||
|
||||
* Initial support for lenient parsing (setting this parser option will catch some exceptions internally and recover the parser’s state as neatly as possible).
|
||||
* *No backwards-incompatible changes*
|
||||
* *No deprecations*
|
||||
|
||||
## 5.0.4 (2013-03-21)
|
||||
|
||||
* Don’t output floats with locale-aware separator chars
|
||||
* *No backwards-incompatible changes*
|
||||
* *No deprecations*
|
||||
|
||||
## 5.0.3 (2013-03-21)
|
||||
|
||||
* More size units recognized
|
||||
* *No backwards-incompatible changes*
|
||||
* *No deprecations*
|
||||
|
||||
## 5.0.2 (2013-03-21)
|
||||
|
||||
* CHANGELOG.md file added to distribution
|
||||
* *No backwards-incompatible changes*
|
||||
* *No deprecations*
|
||||
|
||||
## 5.0.1 (2013-03-20)
|
||||
|
||||
* Internal cleanup
|
||||
* *No backwards-incompatible changes*
|
||||
* *No deprecations*
|
||||
|
||||
## 5.0.0 (2013-03-20)
|
||||
|
||||
* Correctly parse all known CSS 3 units (including Hz and kHz).
|
||||
* Output RGB colors in short (#aaa or #ababab) notation
|
||||
* Be case-insensitive when parsing identifiers.
|
||||
* *No deprecations*
|
||||
|
||||
### Backwards-incompatible changes
|
||||
|
||||
* `Sabberworm\CSS\Value\Color`’s `__toString` method overrides `CSSList`’s to maybe return something other than `type(value, …)` (see above).
|
||||
|
||||
## 4.0.0 (2013-03-19)
|
||||
|
||||
* Support for more @-rules
|
||||
* Generic interface `Sabberworm\CSS\Property\AtRule`, implemented by all @-rule classes
|
||||
* *No deprecations*
|
||||
|
||||
### Backwards-incompatible changes
|
||||
|
||||
* `Sabberworm\CSS\RuleSet\AtRule` renamed to `Sabberworm\CSS\RuleSet\AtRuleSet`
|
||||
* `Sabberworm\CSS\CSSList\MediaQuery` renamed to `Sabberworm\CSS\RuleSet\CSSList\AtRuleBlockList` with differing semantics and API (which also works for other block-list-based @-rules like `@supports`).
|
||||
|
||||
## 3.0.0 (2013-03-06)
|
||||
|
||||
* Support for lenient parsing (on by default)
|
||||
* *No deprecations*
|
||||
|
||||
### Backwards-incompatible changes
|
||||
|
||||
* All properties (like whether or not to use `mb_`-functions, which default charset to use and – new – whether or not to be forgiving when parsing) are now encapsulated in an instance of `Sabberworm\CSS\Settings` which can be passed as the second argument to `Sabberworm\CSS\Parser->__construct()`.
|
||||
* Specifying a charset as the second argument to `Sabberworm\CSS\Parser->__construct()` is no longer supported. Use `Sabberworm\CSS\Settings::create()->withDefaultCharset('some-charset')` instead.
|
||||
* Setting `Sabberworm\CSS\Parser->bUseMbFunctions` has no effect. Use `Sabberworm\CSS\Settings::create()->withMultibyteSupport(true/false)` instead.
|
||||
* `Sabberworm\CSS\Parser->parse()` may throw a `Sabberworm\CSS\Parsing\UnexpectedTokenException` when in strict parsing mode.
|
||||
|
||||
## 2.0.0 (2013-01-29)
|
||||
|
||||
* Allow multiple rules of the same type per rule set
|
||||
|
||||
### Backwards-incompatible changes
|
||||
|
||||
* `Sabberworm\CSS\RuleSet->getRules()` returns an index-based array instead of an associative array. Use `Sabberworm\CSS\RuleSet->getRulesAssoc()` (which eliminates duplicate rules and lets the later rule of the same name win).
|
||||
* `Sabberworm\CSS\RuleSet->removeRule()` works as it did before except when passed an instance of `Sabberworm\CSS\Rule\Rule`, in which case it would only remove the exact rule given instead of all the rules of the same type. To get the old behaviour, use `Sabberworm\CSS\RuleSet->removeRule($oRule->getRule()`;
|
||||
|
||||
## 1.0
|
||||
|
||||
Initial release of a stable public API.
|
||||
|
||||
## 0.9
|
||||
|
||||
Last version not to use PSR-0 project organization semantics.
|
||||
21
lib/sabberworm/php-css-parser/LICENSE
Normal file
21
lib/sabberworm/php-css-parser/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2011 Raphael Schweikert, https://www.sabberworm.com/
|
||||
|
||||
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.
|
||||
632
lib/sabberworm/php-css-parser/README.md
Normal file
632
lib/sabberworm/php-css-parser/README.md
Normal file
@@ -0,0 +1,632 @@
|
||||
# PHP CSS Parser
|
||||
|
||||
[](https://github.com/sabberworm/PHP-CSS-Parser/actions/)
|
||||
|
||||
A Parser for CSS Files written in PHP. Allows extraction of CSS files into a data structure, manipulation of said structure and output as (optimized) CSS.
|
||||
|
||||
## Usage
|
||||
|
||||
### Installation using Composer
|
||||
|
||||
```bash
|
||||
composer require sabberworm/php-css-parser
|
||||
```
|
||||
|
||||
### Extraction
|
||||
|
||||
To use the CSS Parser, create a new instance. The constructor takes the following form:
|
||||
|
||||
```php
|
||||
new \Sabberworm\CSS\Parser($css);
|
||||
```
|
||||
|
||||
To read a file, for example, you’d do the following:
|
||||
|
||||
```php
|
||||
$parser = new \Sabberworm\CSS\Parser(file_get_contents('somefile.css'));
|
||||
$cssDocument = $parser->parse();
|
||||
```
|
||||
|
||||
The resulting CSS document structure can be manipulated prior to being output.
|
||||
|
||||
### Options
|
||||
|
||||
#### Charset
|
||||
|
||||
The charset option is used only if no `@charset` declaration is found in the CSS file. UTF-8 is the default, so you won’t have to create a settings object at all if you don’t intend to change that.
|
||||
|
||||
```php
|
||||
$settings = \Sabberworm\CSS\Settings::create()
|
||||
->withDefaultCharset('windows-1252');
|
||||
$parser = new \Sabberworm\CSS\Parser($css, $settings);
|
||||
```
|
||||
|
||||
#### Strict parsing
|
||||
|
||||
To have the parser choke on invalid rules, supply a thusly configured `\Sabberworm\CSS\Settings` object:
|
||||
|
||||
```php
|
||||
$parser = new \Sabberworm\CSS\Parser(
|
||||
file_get_contents('somefile.css'),
|
||||
\Sabberworm\CSS\Settings::create()->beStrict()
|
||||
);
|
||||
```
|
||||
|
||||
#### Disable multibyte functions
|
||||
|
||||
To achieve faster parsing, you can choose to have PHP-CSS-Parser use regular string functions instead of `mb_*` functions. This should work fine in most cases, even for UTF-8 files, as all the multibyte characters are in string literals. Still it’s not recommended using this with input you have no control over as it’s not thoroughly covered by test cases.
|
||||
|
||||
```php
|
||||
$settings = \Sabberworm\CSS\Settings::create()->withMultibyteSupport(false);
|
||||
$parser = new \Sabberworm\CSS\Parser($css, $settings);
|
||||
```
|
||||
|
||||
### Manipulation
|
||||
|
||||
The resulting data structure consists mainly of five basic types: `CSSList`, `RuleSet`, `Rule`, `Selector` and `Value`. There are two additional types used: `Import` and `Charset`, which you won’t use often.
|
||||
|
||||
#### CSSList
|
||||
|
||||
`CSSList` represents a generic CSS container, most likely containing declaration blocks (rule sets with a selector), but it may also contain at-rules, charset declarations, etc. `CSSList` has the following concrete subtypes:
|
||||
|
||||
* `Document` – representing the root of a CSS file.
|
||||
* `MediaQuery` – represents a subsection of a `CSSList` that only applies to an output device matching the contained media query.
|
||||
|
||||
To access the items stored in a `CSSList` – like the document you got back when calling `$parser->parse()` –, use `getContents()`, then iterate over that collection and use instanceof to check whether you’re dealing with another `CSSList`, a `RuleSet`, a `Import` or a `Charset`.
|
||||
|
||||
To append a new item (selector, media query, etc.) to an existing `CSSList`, construct it using the constructor for this class and use the `append($oItem)` method.
|
||||
|
||||
#### RuleSet
|
||||
|
||||
`RuleSet` is a container for individual rules. The most common form of a rule set is one constrained by a selector. The following concrete subtypes exist:
|
||||
|
||||
* `AtRuleSet` – for generic at-rules which do not match the ones specifically mentioned like `@import`, `@charset` or `@media`. A common example for this is `@font-face`.
|
||||
* `DeclarationBlock` – a `RuleSet` constrained by a `Selector`; contains an array of selector objects (comma-separated in the CSS) as well as the rules to be applied to the matching elements.
|
||||
|
||||
Note: A `CSSList` can contain other `CSSList`s (and `Import`s as well as a `Charset`), while a `RuleSet` can only contain `Rule`s.
|
||||
|
||||
If you want to manipulate a `RuleSet`, use the methods `addRule(Rule $rule)`, `getRules()` and `removeRule($rule)` (which accepts either a `Rule` instance or a rule name; optionally suffixed by a dash to remove all related rules).
|
||||
|
||||
#### Rule
|
||||
|
||||
`Rule`s just have a key (the rule) and a value. These values are all instances of a `Value`.
|
||||
|
||||
#### Value
|
||||
|
||||
`Value` is an abstract class that only defines the `render` method. The concrete subclasses for atomic value types are:
|
||||
|
||||
* `Size` – consists of a numeric `size` value and a unit.
|
||||
* `Color` – colors 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.
|
||||
* `CSSString` – this is just a wrapper for quoted strings to distinguish them from keywords; always output with double quotes.
|
||||
* `URL` – URLs in CSS; always output in URL("") notation.
|
||||
|
||||
There is another abstract subclass of `Value`, `ValueList`. A `ValueList` represents a lists of `Value`s, separated by some separation character (mostly `,`, whitespace, or `/`). There are two types of `ValueList`s:
|
||||
|
||||
* `RuleValueList` – The default type, used to represent all multi-valued 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).
|
||||
* `CSSFunction` – A special kind of value that also contains a function name and where the values are the function’s arguments. Also handles equals-sign-separated argument lists like `filter: alpha(opacity=90);`.
|
||||
|
||||
#### Convenience methods
|
||||
|
||||
There are a few convenience methods on Document to ease finding, manipulating and deleting rules:
|
||||
|
||||
* `getAllDeclarationBlocks()` – does what it says; no matter how deeply nested your selectors are. Aliased as `getAllSelectors()`.
|
||||
* `getAllRuleSets()` – does what it says; no matter how deeply nested your rule sets are.
|
||||
* `getAllValues()` – finds all `Value` objects inside `Rule`s.
|
||||
|
||||
## To-Do
|
||||
|
||||
* More convenience methods (like `selectorsWithElement($sId/Class/TagName)`, `attributesOfType($type)`, `removeAttributesOfType($type)`)
|
||||
* Real multibyte support. Currently, only multibyte charsets whose first 255 code points take up only one byte and are identical with ASCII are supported (yes, UTF-8 fits this description).
|
||||
* Named color support (using `Color` instead of an anonymous string literal)
|
||||
|
||||
## Use cases
|
||||
|
||||
### Use `Parser` to prepend an ID to all selectors
|
||||
|
||||
```php
|
||||
$myId = "#my_id";
|
||||
$parser = new \Sabberworm\CSS\Parser($css);
|
||||
$cssDocument = $parser->parse();
|
||||
foreach ($cssDocument->getAllDeclarationBlocks() as $block) {
|
||||
foreach ($block->getSelectors() as $selector) {
|
||||
// Loop over all selector parts (the comma-separated strings in a
|
||||
// selector) and prepend the ID.
|
||||
$selector->setSelector($myId.' '.$selector->getSelector());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Shrink all absolute sizes to half
|
||||
|
||||
```php
|
||||
$parser = new \Sabberworm\CSS\Parser($css);
|
||||
$cssDocument = $parser->parse();
|
||||
foreach ($cssDocument->getAllValues() as $value) {
|
||||
if ($value instanceof CSSSize && !$value->isRelative()) {
|
||||
$value->setSize($value->getSize() / 2);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Remove unwanted rules
|
||||
|
||||
```php
|
||||
$parser = new \Sabberworm\CSS\Parser($css);
|
||||
$cssDocument = $parser->parse();
|
||||
foreach($cssDocument->getAllRuleSets() as $oRuleSet) {
|
||||
// Note that the added dash will make this remove all rules starting with
|
||||
// `font-` (like `font-size`, `font-weight`, etc.) as well as a potential
|
||||
// `font-rule`.
|
||||
$oRuleSet->removeRule('font-');
|
||||
$oRuleSet->removeRule('cursor');
|
||||
}
|
||||
```
|
||||
|
||||
### Output
|
||||
|
||||
To output the entire CSS document into a variable, just use `->render()`:
|
||||
|
||||
```php
|
||||
$parser = new \Sabberworm\CSS\Parser(file_get_contents('somefile.css'));
|
||||
$cssDocument = $parser->parse();
|
||||
print $cssDocument->render();
|
||||
```
|
||||
|
||||
If you want to format the output, pass an instance of type `\Sabberworm\CSS\OutputFormat`:
|
||||
|
||||
```php
|
||||
$format = \Sabberworm\CSS\OutputFormat::create()
|
||||
->indentWithSpaces(4)->setSpaceBetweenRules("\n");
|
||||
print $cssDocument->render($format);
|
||||
```
|
||||
|
||||
Or use one of the predefined formats:
|
||||
|
||||
```php
|
||||
print $cssDocument->render(Sabberworm\CSS\OutputFormat::createPretty());
|
||||
print $cssDocument->render(Sabberworm\CSS\OutputFormat::createCompact());
|
||||
```
|
||||
|
||||
To see what you can do with output formatting, look at the tests in `tests/OutputFormatTest.php`.
|
||||
|
||||
## Examples
|
||||
|
||||
### Example 1 (At-Rules)
|
||||
|
||||
#### Input
|
||||
|
||||
```css
|
||||
@charset "utf-8";
|
||||
|
||||
@font-face {
|
||||
font-family: "CrassRoots";
|
||||
src: url("../media/cr.ttf");
|
||||
}
|
||||
|
||||
html, body {
|
||||
font-size: 1.6em;
|
||||
}
|
||||
|
||||
@keyframes mymove {
|
||||
from { top: 0px; }
|
||||
to { top: 200px; }
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
#### Structure (`var_dump()`)
|
||||
|
||||
```php
|
||||
class Sabberworm\CSS\CSSList\Document#4 (2) {
|
||||
protected $aContents =>
|
||||
array(4) {
|
||||
[0] =>
|
||||
class Sabberworm\CSS\Property\Charset#6 (2) {
|
||||
private $sCharset =>
|
||||
class Sabberworm\CSS\Value\CSSString#5 (2) {
|
||||
private $sString =>
|
||||
string(5) "utf-8"
|
||||
protected $iLineNo =>
|
||||
int(1)
|
||||
}
|
||||
protected $iLineNo =>
|
||||
int(1)
|
||||
}
|
||||
[1] =>
|
||||
class Sabberworm\CSS\RuleSet\AtRuleSet#7 (4) {
|
||||
private $sType =>
|
||||
string(9) "font-face"
|
||||
private $sArgs =>
|
||||
string(0) ""
|
||||
private $aRules =>
|
||||
array(2) {
|
||||
'font-family' =>
|
||||
array(1) {
|
||||
[0] =>
|
||||
class Sabberworm\CSS\Rule\Rule#8 (4) {
|
||||
private $sRule =>
|
||||
string(11) "font-family"
|
||||
private $mValue =>
|
||||
class Sabberworm\CSS\Value\CSSString#9 (2) {
|
||||
private $sString =>
|
||||
string(10) "CrassRoots"
|
||||
protected $iLineNo =>
|
||||
int(4)
|
||||
}
|
||||
private $bIsImportant =>
|
||||
bool(false)
|
||||
protected $iLineNo =>
|
||||
int(4)
|
||||
}
|
||||
}
|
||||
'src' =>
|
||||
array(1) {
|
||||
[0] =>
|
||||
class Sabberworm\CSS\Rule\Rule#10 (4) {
|
||||
private $sRule =>
|
||||
string(3) "src"
|
||||
private $mValue =>
|
||||
class Sabberworm\CSS\Value\URL#11 (2) {
|
||||
private $oURL =>
|
||||
class Sabberworm\CSS\Value\CSSString#12 (2) {
|
||||
private $sString =>
|
||||
string(15) "../media/cr.ttf"
|
||||
protected $iLineNo =>
|
||||
int(5)
|
||||
}
|
||||
protected $iLineNo =>
|
||||
int(5)
|
||||
}
|
||||
private $bIsImportant =>
|
||||
bool(false)
|
||||
protected $iLineNo =>
|
||||
int(5)
|
||||
}
|
||||
}
|
||||
}
|
||||
protected $iLineNo =>
|
||||
int(3)
|
||||
}
|
||||
[2] =>
|
||||
class Sabberworm\CSS\RuleSet\DeclarationBlock#13 (3) {
|
||||
private $aSelectors =>
|
||||
array(2) {
|
||||
[0] =>
|
||||
class Sabberworm\CSS\Property\Selector#14 (2) {
|
||||
private $sSelector =>
|
||||
string(4) "html"
|
||||
private $iSpecificity =>
|
||||
NULL
|
||||
}
|
||||
[1] =>
|
||||
class Sabberworm\CSS\Property\Selector#15 (2) {
|
||||
private $sSelector =>
|
||||
string(4) "body"
|
||||
private $iSpecificity =>
|
||||
NULL
|
||||
}
|
||||
}
|
||||
private $aRules =>
|
||||
array(1) {
|
||||
'font-size' =>
|
||||
array(1) {
|
||||
[0] =>
|
||||
class Sabberworm\CSS\Rule\Rule#16 (4) {
|
||||
private $sRule =>
|
||||
string(9) "font-size"
|
||||
private $mValue =>
|
||||
class Sabberworm\CSS\Value\Size#17 (4) {
|
||||
private $fSize =>
|
||||
double(1.6)
|
||||
private $sUnit =>
|
||||
string(2) "em"
|
||||
private $bIsColorComponent =>
|
||||
bool(false)
|
||||
protected $iLineNo =>
|
||||
int(9)
|
||||
}
|
||||
private $bIsImportant =>
|
||||
bool(false)
|
||||
protected $iLineNo =>
|
||||
int(9)
|
||||
}
|
||||
}
|
||||
}
|
||||
protected $iLineNo =>
|
||||
int(8)
|
||||
}
|
||||
[3] =>
|
||||
class Sabberworm\CSS\CSSList\KeyFrame#18 (4) {
|
||||
private $vendorKeyFrame =>
|
||||
string(9) "keyframes"
|
||||
private $animationName =>
|
||||
string(6) "mymove"
|
||||
protected $aContents =>
|
||||
array(2) {
|
||||
[0] =>
|
||||
class Sabberworm\CSS\RuleSet\DeclarationBlock#19 (3) {
|
||||
private $aSelectors =>
|
||||
array(1) {
|
||||
[0] =>
|
||||
class Sabberworm\CSS\Property\Selector#20 (2) {
|
||||
private $sSelector =>
|
||||
string(4) "from"
|
||||
private $iSpecificity =>
|
||||
NULL
|
||||
}
|
||||
}
|
||||
private $aRules =>
|
||||
array(1) {
|
||||
'top' =>
|
||||
array(1) {
|
||||
[0] =>
|
||||
class Sabberworm\CSS\Rule\Rule#21 (4) {
|
||||
private $sRule =>
|
||||
string(3) "top"
|
||||
private $mValue =>
|
||||
class Sabberworm\CSS\Value\Size#22 (4) {
|
||||
private $fSize =>
|
||||
double(0)
|
||||
private $sUnit =>
|
||||
string(2) "px"
|
||||
private $bIsColorComponent =>
|
||||
bool(false)
|
||||
protected $iLineNo =>
|
||||
int(13)
|
||||
}
|
||||
private $bIsImportant =>
|
||||
bool(false)
|
||||
protected $iLineNo =>
|
||||
int(13)
|
||||
}
|
||||
}
|
||||
}
|
||||
protected $iLineNo =>
|
||||
int(13)
|
||||
}
|
||||
[1] =>
|
||||
class Sabberworm\CSS\RuleSet\DeclarationBlock#23 (3) {
|
||||
private $aSelectors =>
|
||||
array(1) {
|
||||
[0] =>
|
||||
class Sabberworm\CSS\Property\Selector#24 (2) {
|
||||
private $sSelector =>
|
||||
string(2) "to"
|
||||
private $iSpecificity =>
|
||||
NULL
|
||||
}
|
||||
}
|
||||
private $aRules =>
|
||||
array(1) {
|
||||
'top' =>
|
||||
array(1) {
|
||||
[0] =>
|
||||
class Sabberworm\CSS\Rule\Rule#25 (4) {
|
||||
private $sRule =>
|
||||
string(3) "top"
|
||||
private $mValue =>
|
||||
class Sabberworm\CSS\Value\Size#26 (4) {
|
||||
private $fSize =>
|
||||
double(200)
|
||||
private $sUnit =>
|
||||
string(2) "px"
|
||||
private $bIsColorComponent =>
|
||||
bool(false)
|
||||
protected $iLineNo =>
|
||||
int(14)
|
||||
}
|
||||
private $bIsImportant =>
|
||||
bool(false)
|
||||
protected $iLineNo =>
|
||||
int(14)
|
||||
}
|
||||
}
|
||||
}
|
||||
protected $iLineNo =>
|
||||
int(14)
|
||||
}
|
||||
}
|
||||
protected $iLineNo =>
|
||||
int(12)
|
||||
}
|
||||
}
|
||||
protected $iLineNo =>
|
||||
int(1)
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
#### Output (`render()`)
|
||||
|
||||
```css
|
||||
@charset "utf-8";
|
||||
@font-face {font-family: "CrassRoots";src: url("../media/cr.ttf");}
|
||||
html, body {font-size: 1.6em;}
|
||||
@keyframes mymove {from {top: 0px;} to {top: 200px;}}
|
||||
```
|
||||
|
||||
### Example 2 (Values)
|
||||
|
||||
#### Input
|
||||
|
||||
```css
|
||||
#header {
|
||||
margin: 10px 2em 1cm 2%;
|
||||
font-family: Verdana, Helvetica, "Gill Sans", sans-serif;
|
||||
color: red !important;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
#### Structure (`var_dump()`)
|
||||
|
||||
```php
|
||||
class Sabberworm\CSS\CSSList\Document#4 (2) {
|
||||
protected $aContents =>
|
||||
array(1) {
|
||||
[0] =>
|
||||
class Sabberworm\CSS\RuleSet\DeclarationBlock#5 (3) {
|
||||
private $aSelectors =>
|
||||
array(1) {
|
||||
[0] =>
|
||||
class Sabberworm\CSS\Property\Selector#6 (2) {
|
||||
private $sSelector =>
|
||||
string(7) "#header"
|
||||
private $iSpecificity =>
|
||||
NULL
|
||||
}
|
||||
}
|
||||
private $aRules =>
|
||||
array(3) {
|
||||
'margin' =>
|
||||
array(1) {
|
||||
[0] =>
|
||||
class Sabberworm\CSS\Rule\Rule#7 (4) {
|
||||
private $sRule =>
|
||||
string(6) "margin"
|
||||
private $mValue =>
|
||||
class Sabberworm\CSS\Value\RuleValueList#12 (3) {
|
||||
protected $aComponents =>
|
||||
array(4) {
|
||||
[0] =>
|
||||
class Sabberworm\CSS\Value\Size#8 (4) {
|
||||
private $fSize =>
|
||||
double(10)
|
||||
private $sUnit =>
|
||||
string(2) "px"
|
||||
private $bIsColorComponent =>
|
||||
bool(false)
|
||||
protected $iLineNo =>
|
||||
int(2)
|
||||
}
|
||||
[1] =>
|
||||
class Sabberworm\CSS\Value\Size#9 (4) {
|
||||
private $fSize =>
|
||||
double(2)
|
||||
private $sUnit =>
|
||||
string(2) "em"
|
||||
private $bIsColorComponent =>
|
||||
bool(false)
|
||||
protected $iLineNo =>
|
||||
int(2)
|
||||
}
|
||||
[2] =>
|
||||
class Sabberworm\CSS\Value\Size#10 (4) {
|
||||
private $fSize =>
|
||||
double(1)
|
||||
private $sUnit =>
|
||||
string(2) "cm"
|
||||
private $bIsColorComponent =>
|
||||
bool(false)
|
||||
protected $iLineNo =>
|
||||
int(2)
|
||||
}
|
||||
[3] =>
|
||||
class Sabberworm\CSS\Value\Size#11 (4) {
|
||||
private $fSize =>
|
||||
double(2)
|
||||
private $sUnit =>
|
||||
string(1) "%"
|
||||
private $bIsColorComponent =>
|
||||
bool(false)
|
||||
protected $iLineNo =>
|
||||
int(2)
|
||||
}
|
||||
}
|
||||
protected $sSeparator =>
|
||||
string(1) " "
|
||||
protected $iLineNo =>
|
||||
int(2)
|
||||
}
|
||||
private $bIsImportant =>
|
||||
bool(false)
|
||||
protected $iLineNo =>
|
||||
int(2)
|
||||
}
|
||||
}
|
||||
'font-family' =>
|
||||
array(1) {
|
||||
[0] =>
|
||||
class Sabberworm\CSS\Rule\Rule#13 (4) {
|
||||
private $sRule =>
|
||||
string(11) "font-family"
|
||||
private $mValue =>
|
||||
class Sabberworm\CSS\Value\RuleValueList#15 (3) {
|
||||
protected $aComponents =>
|
||||
array(4) {
|
||||
[0] =>
|
||||
string(7) "Verdana"
|
||||
[1] =>
|
||||
string(9) "Helvetica"
|
||||
[2] =>
|
||||
class Sabberworm\CSS\Value\CSSString#14 (2) {
|
||||
private $sString =>
|
||||
string(9) "Gill Sans"
|
||||
protected $iLineNo =>
|
||||
int(3)
|
||||
}
|
||||
[3] =>
|
||||
string(10) "sans-serif"
|
||||
}
|
||||
protected $sSeparator =>
|
||||
string(1) ","
|
||||
protected $iLineNo =>
|
||||
int(3)
|
||||
}
|
||||
private $bIsImportant =>
|
||||
bool(false)
|
||||
protected $iLineNo =>
|
||||
int(3)
|
||||
}
|
||||
}
|
||||
'color' =>
|
||||
array(1) {
|
||||
[0] =>
|
||||
class Sabberworm\CSS\Rule\Rule#16 (4) {
|
||||
private $sRule =>
|
||||
string(5) "color"
|
||||
private $mValue =>
|
||||
string(3) "red"
|
||||
private $bIsImportant =>
|
||||
bool(true)
|
||||
protected $iLineNo =>
|
||||
int(4)
|
||||
}
|
||||
}
|
||||
}
|
||||
protected $iLineNo =>
|
||||
int(1)
|
||||
}
|
||||
}
|
||||
protected $iLineNo =>
|
||||
int(1)
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
#### Output (`render()`)
|
||||
|
||||
```css
|
||||
#header {margin: 10px 2em 1cm 2%;font-family: Verdana,Helvetica,"Gill Sans",sans-serif;color: red !important;}
|
||||
```
|
||||
|
||||
## Contributors/Thanks to
|
||||
|
||||
* [oliverklee](https://github.com/oliverklee) for lots of refactorings, code modernizations and CI integrations
|
||||
* [raxbg](https://github.com/raxbg) for contributions to parse `calc`, grid lines, and various bugfixes.
|
||||
* [westonruter](https://github.com/westonruter) for bugfixes and improvements.
|
||||
* [FMCorz](https://github.com/FMCorz) for many patches and suggestions, for being able to parse comments and IE hacks (in lenient mode).
|
||||
* [Lullabot](https://github.com/Lullabot) for a patch that allows to know the line number for each parsed token.
|
||||
* [ju1ius](https://github.com/ju1ius) for the specificity parsing code and the ability to expand/compact shorthand properties.
|
||||
* [ossinkine](https://github.com/ossinkine) for a 150 time performance boost.
|
||||
* [GaryJones](https://github.com/GaryJones) for lots of input and [https://css-specificity.info/](https://css-specificity.info/).
|
||||
* [docteurklein](https://github.com/docteurklein) for output formatting and `CSSList->remove()` inspiration.
|
||||
* [nicolopignatelli](https://github.com/nicolopignatelli) for PSR-0 compatibility.
|
||||
* [diegoembarcadero](https://github.com/diegoembarcadero) for keyframe at-rule parsing.
|
||||
* [goetas](https://github.com/goetas) for @namespace at-rule support.
|
||||
* [View full list](https://github.com/sabberworm/PHP-CSS-Parser/contributors)
|
||||
|
||||
## Misc
|
||||
|
||||
* Legacy Support: The latest pre-PSR-0 version of this project can be checked with the `0.9.0` tag.
|
||||
* Running Tests: To run all unit tests for this project, run `composer install` to install phpunit and use `./vendor/bin/phpunit`.
|
||||
69
lib/sabberworm/php-css-parser/composer.json
Normal file
69
lib/sabberworm/php-css-parser/composer.json
Normal file
@@ -0,0 +1,69 @@
|
||||
{
|
||||
"name": "sabberworm/php-css-parser",
|
||||
"type": "library",
|
||||
"description": "Parser for CSS Files written in PHP",
|
||||
"keywords": [
|
||||
"parser",
|
||||
"css",
|
||||
"stylesheet"
|
||||
],
|
||||
"homepage": "https://www.sabberworm.com/blog/2010/6/10/php-css-parser",
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Raphael Schweikert"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": ">=5.6.20",
|
||||
"ext-iconv": "*"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^4.8.36",
|
||||
"codacy/coverage": "^1.4"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-mbstring": "for parsing UTF-8 CSS"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Sabberworm\\CSS\\": "src/"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Sabberworm\\CSS\\Tests\\": "tests/"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"ci": [
|
||||
"@ci:static"
|
||||
],
|
||||
"ci:php:fixer": "@php ./.phive/php-cs-fixer.phar --config=config/php-cs-fixer.php fix --dry-run -v --show-progress=dots bin src tests",
|
||||
"ci:php:sniffer": "@php ./.phive/phpcs.phar --standard=config/phpcs.xml bin src tests",
|
||||
"ci:php:stan": "@php ./.phive/phpstan.phar --configuration=config/phpstan.neon",
|
||||
"ci:static": [
|
||||
"@ci:php:fixer",
|
||||
"@ci:php:sniffer",
|
||||
"@ci:php:stan"
|
||||
],
|
||||
"fix:php": [
|
||||
"@fix:php:fixer",
|
||||
"@fix:php:sniffer"
|
||||
],
|
||||
"fix:php:fixer": "@php ./.phive/php-cs-fixer.phar --config=config/php-cs-fixer.php fix bin src tests",
|
||||
"fix:php:sniffer": "@php ./.phive/phpcbf.phar --standard=config/phpcs.xml bin src tests",
|
||||
"phpstan:baseline": "@php ./.phive/phpstan.phar --configuration=config/phpstan.neon --generate-baseline=config/phpstan-baseline.neon"
|
||||
},
|
||||
"scripts-descriptions": {
|
||||
"ci": "Runs all dynamic and static code checks (i.e. currently, only the static checks).",
|
||||
"ci:php:fixer": "Checks the code style with PHP CS Fixer.",
|
||||
"ci:php:sniffer": "Checks the code style with PHP_CodeSniffer.",
|
||||
"ci:php:stan": "Checks the types with PHPStan.",
|
||||
"ci:static": "Runs all static code analysis checks for the code.",
|
||||
"fix:php": "Autofixes all autofixable issues in the PHP code.",
|
||||
"fix:php:fixer": "Fixes autofixable issues found by PHP CS Fixer.",
|
||||
"fix:php:sniffer": "Fixes autofixable issues found by PHP_CodeSniffer.",
|
||||
"phpstand:baseline": "Updates the PHPStan baseline file to match the code."
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
namespace Sabberworm\CSS\CSSList;
|
||||
|
||||
use Sabberworm\CSS\OutputFormat;
|
||||
use 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
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
return $this->render(new OutputFormat());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function render(OutputFormat $oOutputFormat)
|
||||
{
|
||||
$sArgs = $this->sArgs;
|
||||
if ($sArgs) {
|
||||
$sArgs = ' ' . $sArgs;
|
||||
}
|
||||
$sResult = $oOutputFormat->sBeforeAtRuleBlock;
|
||||
$sResult .= "@{$this->sType}$sArgs{$oOutputFormat->spaceBeforeOpeningBrace()}{";
|
||||
$sResult .= parent::render($oOutputFormat);
|
||||
$sResult .= '}';
|
||||
$sResult .= $oOutputFormat->sAfterAtRuleBlock;
|
||||
return $sResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isRootList()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
143
lib/sabberworm/php-css-parser/src/CSSList/CSSBlockList.php
Normal file
143
lib/sabberworm/php-css-parser/src/CSSList/CSSBlockList.php
Normal file
@@ -0,0 +1,143 @@
|
||||
<?php
|
||||
|
||||
namespace Sabberworm\CSS\CSSList;
|
||||
|
||||
use Sabberworm\CSS\Property\Selector;
|
||||
use Sabberworm\CSS\Rule\Rule;
|
||||
use Sabberworm\CSS\RuleSet\DeclarationBlock;
|
||||
use Sabberworm\CSS\RuleSet\RuleSet;
|
||||
use Sabberworm\CSS\Value\CSSFunction;
|
||||
use Sabberworm\CSS\Value\Value;
|
||||
use 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param CSSList|Rule|RuleSet|Value $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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
479
lib/sabberworm/php-css-parser/src/CSSList/CSSList.php
Normal file
479
lib/sabberworm/php-css-parser/src/CSSList/CSSList.php
Normal file
@@ -0,0 +1,479 @@
|
||||
<?php
|
||||
|
||||
namespace Sabberworm\CSS\CSSList;
|
||||
|
||||
use Sabberworm\CSS\Comment\Comment;
|
||||
use Sabberworm\CSS\Comment\Commentable;
|
||||
use Sabberworm\CSS\OutputFormat;
|
||||
use Sabberworm\CSS\Parsing\ParserState;
|
||||
use Sabberworm\CSS\Parsing\SourceException;
|
||||
use Sabberworm\CSS\Parsing\UnexpectedEOFException;
|
||||
use Sabberworm\CSS\Parsing\UnexpectedTokenException;
|
||||
use Sabberworm\CSS\Property\AtRule;
|
||||
use Sabberworm\CSS\Property\Charset;
|
||||
use Sabberworm\CSS\Property\CSSNamespace;
|
||||
use Sabberworm\CSS\Property\Import;
|
||||
use Sabberworm\CSS\Property\Selector;
|
||||
use Sabberworm\CSS\Renderable;
|
||||
use Sabberworm\CSS\RuleSet\AtRuleSet;
|
||||
use Sabberworm\CSS\RuleSet\DeclarationBlock;
|
||||
use Sabberworm\CSS\RuleSet\RuleSet;
|
||||
use Sabberworm\CSS\Settings;
|
||||
use Sabberworm\CSS\Value\CSSString;
|
||||
use Sabberworm\CSS\Value\URL;
|
||||
use Sabberworm\CSS\Value\Value;
|
||||
|
||||
/**
|
||||
* A `CSSList` is the most generic container available. Its contents include `RuleSet` as well as other `CSSList`
|
||||
* objects.
|
||||
*
|
||||
* Also, it may contain `Import` and `Charset` objects stemming from at-rules.
|
||||
*/
|
||||
abstract class CSSList implements Renderable, Commentable
|
||||
{
|
||||
/**
|
||||
* @var array<array-key, Comment>
|
||||
*/
|
||||
protected $aComments;
|
||||
|
||||
/**
|
||||
* @var array<int, RuleSet|CSSList|Import|Charset>
|
||||
*/
|
||||
protected $aContents;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
protected $iLineNo;
|
||||
|
||||
/**
|
||||
* @param int $iLineNo
|
||||
*/
|
||||
public function __construct($iLineNo = 0)
|
||||
{
|
||||
$this->aComments = [];
|
||||
$this->aContents = [];
|
||||
$this->iLineNo = $iLineNo;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*
|
||||
* @throws UnexpectedTokenException
|
||||
* @throws SourceException
|
||||
*/
|
||||
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;
|
||||
while (!$oParserState->isEnd()) {
|
||||
$comments = $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->setComments($comments);
|
||||
$oList->append($oListItem);
|
||||
}
|
||||
$oParserState->consumeWhiteSpace();
|
||||
}
|
||||
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()->getString());
|
||||
}
|
||||
return $oAtRule;
|
||||
} elseif ($oParserState->comes('}')) {
|
||||
if (!$oParserState->getSettings()->bLenientParsing) {
|
||||
throw new UnexpectedTokenException('CSS selector', '}', 'identifier', $oParserState->currentLine());
|
||||
} else {
|
||||
if ($bIsRoot) {
|
||||
if ($oParserState->getSettings()->bLenientParsing) {
|
||||
return DeclarationBlock::parse($oParserState);
|
||||
} else {
|
||||
throw new SourceException("Unopened {", $oParserState->currentLine());
|
||||
}
|
||||
} else {
|
||||
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') {
|
||||
$sCharset = CSSString::parse($oParserState);
|
||||
$oParserState->consumeWhiteSpace();
|
||||
$oParserState->consumeUntil([';', ParserState::EOF], true, true);
|
||||
return new Charset($sCharset, $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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getLineNo()
|
||||
{
|
||||
return $this->iLineNo;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 tje 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
return $this->render(new OutputFormat());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function render(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();
|
||||
|
||||
/**
|
||||
* @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;
|
||||
}
|
||||
}
|
||||
172
lib/sabberworm/php-css-parser/src/CSSList/Document.php
Normal file
172
lib/sabberworm/php-css-parser/src/CSSList/Document.php
Normal file
@@ -0,0 +1,172 @@
|
||||
<?php
|
||||
|
||||
namespace Sabberworm\CSS\CSSList;
|
||||
|
||||
use Sabberworm\CSS\OutputFormat;
|
||||
use Sabberworm\CSS\Parsing\ParserState;
|
||||
use Sabberworm\CSS\Parsing\SourceException;
|
||||
use Sabberworm\CSS\Property\Selector;
|
||||
use Sabberworm\CSS\RuleSet\DeclarationBlock;
|
||||
use Sabberworm\CSS\RuleSet\RuleSet;
|
||||
use Sabberworm\CSS\Value\Value;
|
||||
|
||||
/**
|
||||
* The root `CSSList` of a parsed file. Contains all top-level CSS contents, mostly declaration blocks,
|
||||
* but also any at-rules encountered.
|
||||
*/
|
||||
class Document extends CSSBlockList
|
||||
{
|
||||
/**
|
||||
* @param int $iLineNo
|
||||
*/
|
||||
public function __construct($iLineNo = 0)
|
||||
{
|
||||
parent::__construct($iLineNo);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Document
|
||||
*
|
||||
* @throws SourceException
|
||||
*/
|
||||
public static function parse(ParserState $oParserState)
|
||||
{
|
||||
$oDocument = new Document($oParserState->currentLine());
|
||||
CSSList::parseList($oParserState, $oDocument);
|
||||
return $oDocument;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all `DeclarationBlock` objects recursively.
|
||||
*
|
||||
* @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 found recursively in the tree.
|
||||
*
|
||||
* @return array<int, RuleSet>
|
||||
*/
|
||||
public function getAllRuleSets()
|
||||
{
|
||||
/** @var array<int, RuleSet> $aResult */
|
||||
$aResult = [];
|
||||
$this->allRuleSets($aResult);
|
||||
return $aResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all `Value` objects found recursively in the tree.
|
||||
*
|
||||
* @param CSSList|RuleSet|string $mElement
|
||||
* the `CSSList` or `RuleSet` to start the search from (defaults to the whole document).
|
||||
* If a string is given, it is used as rule name filter.
|
||||
* @param bool $bSearchInFunctionArguments whether to also return Value objects used as Function arguments.
|
||||
*
|
||||
* @return array<int, Value>
|
||||
*
|
||||
* @see RuleSet->getRules()
|
||||
*/
|
||||
public function getAllValues($mElement = null, $bSearchInFunctionArguments = false)
|
||||
{
|
||||
$sSearchString = null;
|
||||
if ($mElement === null) {
|
||||
$mElement = $this;
|
||||
} elseif (is_string($mElement)) {
|
||||
$sSearchString = $mElement;
|
||||
$mElement = $this;
|
||||
}
|
||||
/** @var array<int, Value> $aResult */
|
||||
$aResult = [];
|
||||
$this->allValues($mElement, $aResult, $sSearchString, $bSearchInFunctionArguments);
|
||||
return $aResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all `Selector` objects 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
|
||||
*/
|
||||
public function expandShorthands()
|
||||
{
|
||||
foreach ($this->getAllDeclarationBlocks() as $oDeclaration) {
|
||||
$oDeclaration->expandShorthands();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create shorthands properties whenever possible.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
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(OutputFormat $oOutputFormat = null)
|
||||
{
|
||||
if ($oOutputFormat === null) {
|
||||
$oOutputFormat = new OutputFormat();
|
||||
}
|
||||
return parent::render($oOutputFormat);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isRootList()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
104
lib/sabberworm/php-css-parser/src/CSSList/KeyFrame.php
Normal file
104
lib/sabberworm/php-css-parser/src/CSSList/KeyFrame.php
Normal file
@@ -0,0 +1,104 @@
|
||||
<?php
|
||||
|
||||
namespace Sabberworm\CSS\CSSList;
|
||||
|
||||
use Sabberworm\CSS\OutputFormat;
|
||||
use 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
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
return $this->render(new OutputFormat());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function render(OutputFormat $oOutputFormat)
|
||||
{
|
||||
$sResult = "@{$this->vendorKeyFrame} {$this->animationName}{$oOutputFormat->spaceBeforeOpeningBrace()}{";
|
||||
$sResult .= parent::render($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;
|
||||
}
|
||||
}
|
||||
71
lib/sabberworm/php-css-parser/src/Comment/Comment.php
Normal file
71
lib/sabberworm/php-css-parser/src/Comment/Comment.php
Normal file
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
namespace Sabberworm\CSS\Comment;
|
||||
|
||||
use Sabberworm\CSS\OutputFormat;
|
||||
use Sabberworm\CSS\Renderable;
|
||||
|
||||
class Comment implements Renderable
|
||||
{
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
protected $iLineNo;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $sComment;
|
||||
|
||||
/**
|
||||
* @param string $sComment
|
||||
* @param int $iLineNo
|
||||
*/
|
||||
public function __construct($sComment = '', $iLineNo = 0)
|
||||
{
|
||||
$this->sComment = $sComment;
|
||||
$this->iLineNo = $iLineNo;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getComment()
|
||||
{
|
||||
return $this->sComment;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getLineNo()
|
||||
{
|
||||
return $this->iLineNo;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $sComment
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setComment($sComment)
|
||||
{
|
||||
$this->sComment = $sComment;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
return $this->render(new OutputFormat());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function render(OutputFormat $oOutputFormat)
|
||||
{
|
||||
return '/*' . $this->sComment . '*/';
|
||||
}
|
||||
}
|
||||
25
lib/sabberworm/php-css-parser/src/Comment/Commentable.php
Normal file
25
lib/sabberworm/php-css-parser/src/Comment/Commentable.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace 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);
|
||||
}
|
||||
334
lib/sabberworm/php-css-parser/src/OutputFormat.php
Normal file
334
lib/sabberworm/php-css-parser/src/OutputFormat.php
Normal file
@@ -0,0 +1,334 @@
|
||||
<?php
|
||||
|
||||
namespace Sabberworm\CSS;
|
||||
|
||||
/**
|
||||
* Class OutputFormat
|
||||
*
|
||||
* @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
|
||||
*/
|
||||
public $sStringQuotingType = '"';
|
||||
|
||||
/**
|
||||
* Output RGB colors in hash notation if possible
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
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
|
||||
*/
|
||||
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");`)
|
||||
*/
|
||||
public $sSpaceAfterRuleName = ' ';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $sSpaceBeforeRules = '';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $sSpaceAfterRules = '';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $sSpaceBetweenRules = '';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $sSpaceBeforeBlocks = '';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $sSpaceAfterBlocks = '';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $sSpaceBetweenBlocks = "\n";
|
||||
|
||||
/**
|
||||
* Content injected in and around at-rule blocks.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $sBeforeAtRuleBlock = '';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $sAfterAtRuleBlock = '';
|
||||
|
||||
/**
|
||||
* This is what’s printed before and after the comma if a declaration block contains multiple selectors.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $sSpaceBeforeSelectorSeparator = '';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $sSpaceAfterSelectorSeparator = ' ';
|
||||
|
||||
/**
|
||||
* This is what’s printed after the comma of value lists
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $sSpaceBeforeListArgumentSeparator = '';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $sSpaceAfterListArgumentSeparator = '';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $sSpaceBeforeOpeningBrace = ' ';
|
||||
|
||||
/**
|
||||
* Content injected in and around declaration blocks.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $sBeforeDeclarationBlock = '';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $sAfterDeclarationBlockSelectors = '';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $sAfterDeclarationBlock = '';
|
||||
|
||||
/**
|
||||
* Indentation character(s) per level. Only applicable if newlines are used in any of the spacing settings.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $sIndentation = "\t";
|
||||
|
||||
/**
|
||||
* Output exceptions.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $bIgnoreExceptions = false;
|
||||
|
||||
/**
|
||||
* @var OutputFormatter|null
|
||||
*/
|
||||
private $oFormatter = null;
|
||||
|
||||
/**
|
||||
* @var OutputFormat|null
|
||||
*/
|
||||
private $oNextLevelFormat = null;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $iIndentationLevel = 0;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $sName
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
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
|
||||
*/
|
||||
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)) {
|
||||
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
|
||||
*/
|
||||
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
|
||||
*/
|
||||
public function getFormatter()
|
||||
{
|
||||
if ($this->oFormatter === null) {
|
||||
$this->oFormatter = new OutputFormatter($this);
|
||||
}
|
||||
return $this->oFormatter;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
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('');
|
||||
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('SpaceAfterListArgumentSeparator', ['default' => '', ',' => ' ']);
|
||||
return $format;
|
||||
}
|
||||
}
|
||||
231
lib/sabberworm/php-css-parser/src/OutputFormatter.php
Normal file
231
lib/sabberworm/php-css-parser/src/OutputFormatter.php
Normal file
@@ -0,0 +1,231 @@
|
||||
<?php
|
||||
|
||||
namespace Sabberworm\CSS;
|
||||
|
||||
use Sabberworm\CSS\Parsing\OutputException;
|
||||
|
||||
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)
|
||||
{
|
||||
return $this->space('BeforeListArgumentSeparator', $sSeparator);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $sSeparator
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function spaceAfterListArgumentSeparator($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 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->level());
|
||||
}
|
||||
}
|
||||
60
lib/sabberworm/php-css-parser/src/Parser.php
Normal file
60
lib/sabberworm/php-css-parser/src/Parser.php
Normal file
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
namespace Sabberworm\CSS;
|
||||
|
||||
use Sabberworm\CSS\CSSList\Document;
|
||||
use Sabberworm\CSS\Parsing\ParserState;
|
||||
use Sabberworm\CSS\Parsing\SourceException;
|
||||
|
||||
/**
|
||||
* This class parses CSS from text into a data structure.
|
||||
*/
|
||||
class Parser
|
||||
{
|
||||
/**
|
||||
* @var ParserState
|
||||
*/
|
||||
private $oParserState;
|
||||
|
||||
/**
|
||||
* @param string $sText
|
||||
* @param Settings|null $oParserSettings
|
||||
* @param int $iLineNo the line number (starting from 1, not from 0)
|
||||
*/
|
||||
public function __construct($sText, Settings $oParserSettings = null, $iLineNo = 1)
|
||||
{
|
||||
if ($oParserSettings === null) {
|
||||
$oParserSettings = Settings::create();
|
||||
}
|
||||
$this->oParserState = new ParserState($sText, $oParserSettings, $iLineNo);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $sCharset
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setCharset($sCharset)
|
||||
{
|
||||
$this->oParserState->setCharset($sCharset);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function getCharset()
|
||||
{
|
||||
// Note: The `return` statement is missing here. This is a bug that needs to be fixed.
|
||||
$this->oParserState->getCharset();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Document
|
||||
*
|
||||
* @throws SourceException
|
||||
*/
|
||||
public function parse()
|
||||
{
|
||||
return Document::parse($this->oParserState);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace 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);
|
||||
}
|
||||
}
|
||||
516
lib/sabberworm/php-css-parser/src/Parsing/ParserState.php
Normal file
516
lib/sabberworm/php-css-parser/src/Parsing/ParserState.php
Normal file
@@ -0,0 +1,516 @@
|
||||
<?php
|
||||
|
||||
namespace Sabberworm\CSS\Parsing;
|
||||
|
||||
use Sabberworm\CSS\Comment\Comment;
|
||||
use Sabberworm\CSS\Settings;
|
||||
|
||||
class ParserState
|
||||
{
|
||||
/**
|
||||
* @var null
|
||||
*/
|
||||
const EOF = null;
|
||||
|
||||
/**
|
||||
* @var Settings
|
||||
*/
|
||||
private $oParserSettings;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $sText;
|
||||
|
||||
/**
|
||||
* @var array<int, string>
|
||||
*/
|
||||
private $aText;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $iCurrentPosition;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $sCharset;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $iLength;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $iLineNo;
|
||||
|
||||
/**
|
||||
* @param string $sText
|
||||
* @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);
|
||||
}
|
||||
|
||||
/**
|
||||
* @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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $bIgnoreCase
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
* @throws UnexpectedTokenException
|
||||
*/
|
||||
public function parseIdentifier($bIgnoreCase = true)
|
||||
{
|
||||
$sResult = $this->parseCharacter(true);
|
||||
if ($sResult === null) {
|
||||
throw new UnexpectedTokenException($sResult, $this->peek(5), 'identifier', $this->iLineNo);
|
||||
}
|
||||
$sCharacter = null;
|
||||
while (($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
|
||||
*/
|
||||
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()
|
||||
{
|
||||
$comments = [];
|
||||
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;
|
||||
}
|
||||
} else {
|
||||
$oComment = $this->consumeComment();
|
||||
}
|
||||
if ($oComment !== false) {
|
||||
$comments[] = $oComment;
|
||||
}
|
||||
} while ($oComment !== false);
|
||||
return $comments;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace Sabberworm\CSS\Parsing;
|
||||
|
||||
class SourceException extends \Exception
|
||||
{
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $iLineNo;
|
||||
|
||||
/**
|
||||
* @param string $sMessage
|
||||
* @param int $iLineNo
|
||||
*/
|
||||
public function __construct($sMessage, $iLineNo = 0)
|
||||
{
|
||||
$this->iLineNo = $iLineNo;
|
||||
if (!empty($iLineNo)) {
|
||||
$sMessage .= " [line no: $iLineNo]";
|
||||
}
|
||||
parent::__construct($sMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getLineNo()
|
||||
{
|
||||
return $this->iLineNo;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace 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
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace 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);
|
||||
}
|
||||
}
|
||||
34
lib/sabberworm/php-css-parser/src/Property/AtRule.php
Normal file
34
lib/sabberworm/php-css-parser/src/Property/AtRule.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace Sabberworm\CSS\Property;
|
||||
|
||||
use Sabberworm\CSS\Comment\Commentable;
|
||||
use 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
|
||||
*/
|
||||
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
|
||||
*/
|
||||
const SET_RULES = 'font-face/counter-style/page/swash/styleset/annotation';
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function atRuleName();
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function atRuleArgs();
|
||||
}
|
||||
154
lib/sabberworm/php-css-parser/src/Property/CSSNamespace.php
Normal file
154
lib/sabberworm/php-css-parser/src/Property/CSSNamespace.php
Normal file
@@ -0,0 +1,154 @@
|
||||
<?php
|
||||
|
||||
namespace Sabberworm\CSS\Property;
|
||||
|
||||
use Sabberworm\CSS\Comment\Comment;
|
||||
use Sabberworm\CSS\OutputFormat;
|
||||
|
||||
/**
|
||||
* `CSSNamespace` represents an `@namespace` rule.
|
||||
*/
|
||||
class CSSNamespace implements AtRule
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $mUrl;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $sPrefix;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $iLineNo;
|
||||
|
||||
/**
|
||||
* @var array<array-key, Comment>
|
||||
*/
|
||||
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->iLineNo = $iLineNo;
|
||||
$this->aComments = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getLineNo()
|
||||
{
|
||||
return $this->iLineNo;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
return $this->render(new OutputFormat());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function render(OutputFormat $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;
|
||||
}
|
||||
}
|
||||
129
lib/sabberworm/php-css-parser/src/Property/Charset.php
Normal file
129
lib/sabberworm/php-css-parser/src/Property/Charset.php
Normal file
@@ -0,0 +1,129 @@
|
||||
<?php
|
||||
|
||||
namespace Sabberworm\CSS\Property;
|
||||
|
||||
use Sabberworm\CSS\Comment\Comment;
|
||||
use Sabberworm\CSS\OutputFormat;
|
||||
|
||||
/**
|
||||
* 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
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $sCharset;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
protected $iLineNo;
|
||||
|
||||
/**
|
||||
* @var array<array-key, Comment>
|
||||
*/
|
||||
protected $aComments;
|
||||
|
||||
/**
|
||||
* @param string $sCharset
|
||||
* @param int $iLineNo
|
||||
*/
|
||||
public function __construct($sCharset, $iLineNo = 0)
|
||||
{
|
||||
$this->sCharset = $sCharset;
|
||||
$this->iLineNo = $iLineNo;
|
||||
$this->aComments = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getLineNo()
|
||||
{
|
||||
return $this->iLineNo;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $sCharset
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setCharset($sCharset)
|
||||
{
|
||||
$this->sCharset = $sCharset;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getCharset()
|
||||
{
|
||||
return $this->sCharset;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
return $this->render(new OutputFormat());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function render(OutputFormat $oOutputFormat)
|
||||
{
|
||||
return "@charset {$this->sCharset->render($oOutputFormat)};";
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function atRuleName()
|
||||
{
|
||||
return 'charset';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function atRuleArgs()
|
||||
{
|
||||
return $this->sCharset;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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;
|
||||
}
|
||||
}
|
||||
137
lib/sabberworm/php-css-parser/src/Property/Import.php
Normal file
137
lib/sabberworm/php-css-parser/src/Property/Import.php
Normal file
@@ -0,0 +1,137 @@
|
||||
<?php
|
||||
|
||||
namespace Sabberworm\CSS\Property;
|
||||
|
||||
use Sabberworm\CSS\Comment\Comment;
|
||||
use Sabberworm\CSS\OutputFormat;
|
||||
use Sabberworm\CSS\Value\URL;
|
||||
|
||||
/**
|
||||
* Class representing an `@import` rule.
|
||||
*/
|
||||
class Import implements AtRule
|
||||
{
|
||||
/**
|
||||
* @var URL
|
||||
*/
|
||||
private $oLocation;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $sMediaQuery;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
protected $iLineNo;
|
||||
|
||||
/**
|
||||
* @var array<array-key, Comment>
|
||||
*/
|
||||
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->iLineNo = $iLineNo;
|
||||
$this->aComments = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getLineNo()
|
||||
{
|
||||
return $this->iLineNo;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param URL $oLocation
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setLocation($oLocation)
|
||||
{
|
||||
$this->oLocation = $oLocation;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return URL
|
||||
*/
|
||||
public function getLocation()
|
||||
{
|
||||
return $this->oLocation;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
return $this->render(new OutputFormat());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function render(OutputFormat $oOutputFormat)
|
||||
{
|
||||
return "@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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace Sabberworm\CSS\Property;
|
||||
|
||||
class KeyframeSelector extends Selector
|
||||
{
|
||||
/**
|
||||
* regexp for specificity calculations
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
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';
|
||||
}
|
||||
138
lib/sabberworm/php-css-parser/src/Property/Selector.php
Normal file
138
lib/sabberworm/php-css-parser/src/Property/Selector.php
Normal file
@@ -0,0 +1,138 @@
|
||||
<?php
|
||||
|
||||
namespace 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
|
||||
*/
|
||||
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
|
||||
*/
|
||||
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
|
||||
*/
|
||||
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
|
||||
*/
|
||||
public static function isValid($sSelector)
|
||||
{
|
||||
return preg_match(static::SELECTOR_VALIDATION_RX, $sSelector);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $sSelector
|
||||
* @param bool $bCalculateSpecificity
|
||||
*/
|
||||
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
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
21
lib/sabberworm/php-css-parser/src/Renderable.php
Normal file
21
lib/sabberworm/php-css-parser/src/Renderable.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace Sabberworm\CSS;
|
||||
|
||||
interface Renderable
|
||||
{
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function __toString();
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function render(OutputFormat $oOutputFormat);
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getLineNo();
|
||||
}
|
||||
392
lib/sabberworm/php-css-parser/src/Rule/Rule.php
Normal file
392
lib/sabberworm/php-css-parser/src/Rule/Rule.php
Normal file
@@ -0,0 +1,392 @@
|
||||
<?php
|
||||
|
||||
namespace Sabberworm\CSS\Rule;
|
||||
|
||||
use Sabberworm\CSS\Comment\Comment;
|
||||
use Sabberworm\CSS\Comment\Commentable;
|
||||
use Sabberworm\CSS\OutputFormat;
|
||||
use Sabberworm\CSS\Parsing\ParserState;
|
||||
use Sabberworm\CSS\Parsing\UnexpectedEOFException;
|
||||
use Sabberworm\CSS\Parsing\UnexpectedTokenException;
|
||||
use Sabberworm\CSS\Renderable;
|
||||
use Sabberworm\CSS\Value\RuleValueList;
|
||||
use Sabberworm\CSS\Value\Value;
|
||||
|
||||
/**
|
||||
* RuleSets contains Rule objects which always have a key and a value.
|
||||
* In CSS, Rules are expressed as follows: “key: value[0][0] value[0][1], value[1][0] value[1][1];”
|
||||
*/
|
||||
class Rule implements Renderable, Commentable
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $sRule;
|
||||
|
||||
/**
|
||||
* @var RuleValueList|null
|
||||
*/
|
||||
private $mValue;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $bIsImportant;
|
||||
|
||||
/**
|
||||
* @var array<int, int>
|
||||
*/
|
||||
private $aIeHack;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
protected $iLineNo;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
protected $iColNo;
|
||||
|
||||
/**
|
||||
* @var array<array-key, Comment>
|
||||
*/
|
||||
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->iLineNo = $iLineNo;
|
||||
$this->iColNo = $iColNo;
|
||||
$this->aComments = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Rule
|
||||
*
|
||||
* @throws UnexpectedEOFException
|
||||
* @throws UnexpectedTokenException
|
||||
*/
|
||||
public static function parse(ParserState $oParserState)
|
||||
{
|
||||
$aComments = $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(';');
|
||||
}
|
||||
$oParserState->consumeWhiteSpace();
|
||||
|
||||
return $oRule;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $sRule
|
||||
*
|
||||
* @return array<int, string>
|
||||
*/
|
||||
private static function listDelimiterForRule($sRule)
|
||||
{
|
||||
if (preg_match('/^font($|-)/', $sRule)) {
|
||||
return [',', '/', ' '];
|
||||
}
|
||||
return [',', ' ', '/'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getLineNo()
|
||||
{
|
||||
return $this->iLineNo;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getColNo()
|
||||
{
|
||||
return $this->iColNo;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $iLine
|
||||
* @param int $iColumn
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setPosition($iLine, $iColumn)
|
||||
{
|
||||
$this->iColNo = $iColumn;
|
||||
$this->iLineNo = $iLine;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $sRule
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setRule($sRule)
|
||||
{
|
||||
$this->sRule = $sRule;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getRule()
|
||||
{
|
||||
return $this->sRule;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return RuleValueList|null
|
||||
*/
|
||||
public function getValue()
|
||||
{
|
||||
return $this->mValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param RuleValueList|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->iLineNo);
|
||||
if ($mCurrentValue) {
|
||||
$this->mValue->addListComponent($mCurrentValue);
|
||||
}
|
||||
}
|
||||
foreach ($mValue as $mValueItem) {
|
||||
$this->mValue->addListComponent($mValueItem);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $iModifier
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function addIeHack($iModifier)
|
||||
{
|
||||
$this->aIeHack[] = $iModifier;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, int> $aModifiers
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setIeHack(array $aModifiers)
|
||||
{
|
||||
$this->aIeHack = $aModifiers;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, int>
|
||||
*/
|
||||
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
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
return $this->render(new OutputFormat());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function render(OutputFormat $oOutputFormat)
|
||||
{
|
||||
$sResult = "{$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;
|
||||
}
|
||||
}
|
||||
73
lib/sabberworm/php-css-parser/src/RuleSet/AtRuleSet.php
Normal file
73
lib/sabberworm/php-css-parser/src/RuleSet/AtRuleSet.php
Normal file
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
namespace Sabberworm\CSS\RuleSet;
|
||||
|
||||
use Sabberworm\CSS\OutputFormat;
|
||||
use Sabberworm\CSS\Property\AtRule;
|
||||
|
||||
/**
|
||||
* A RuleSet constructed by an unknown at-rule. `@font-face` rules are rendered into AtRuleSet objects.
|
||||
*/
|
||||
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
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
return $this->render(new OutputFormat());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function render(OutputFormat $oOutputFormat)
|
||||
{
|
||||
$sArgs = $this->sArgs;
|
||||
if ($sArgs) {
|
||||
$sArgs = ' ' . $sArgs;
|
||||
}
|
||||
$sResult = "@{$this->sType}$sArgs{$oOutputFormat->spaceBeforeOpeningBrace()}{";
|
||||
$sResult .= parent::render($oOutputFormat);
|
||||
$sResult .= '}';
|
||||
return $sResult;
|
||||
}
|
||||
}
|
||||
831
lib/sabberworm/php-css-parser/src/RuleSet/DeclarationBlock.php
Normal file
831
lib/sabberworm/php-css-parser/src/RuleSet/DeclarationBlock.php
Normal file
@@ -0,0 +1,831 @@
|
||||
<?php
|
||||
|
||||
namespace Sabberworm\CSS\RuleSet;
|
||||
|
||||
use Sabberworm\CSS\CSSList\CSSList;
|
||||
use Sabberworm\CSS\CSSList\KeyFrame;
|
||||
use Sabberworm\CSS\OutputFormat;
|
||||
use Sabberworm\CSS\Parsing\OutputException;
|
||||
use Sabberworm\CSS\Parsing\ParserState;
|
||||
use Sabberworm\CSS\Parsing\UnexpectedEOFException;
|
||||
use Sabberworm\CSS\Parsing\UnexpectedTokenException;
|
||||
use Sabberworm\CSS\Property\KeyframeSelector;
|
||||
use Sabberworm\CSS\Property\Selector;
|
||||
use Sabberworm\CSS\Rule\Rule;
|
||||
use Sabberworm\CSS\Value\Color;
|
||||
use Sabberworm\CSS\Value\RuleValueList;
|
||||
use Sabberworm\CSS\Value\Size;
|
||||
use Sabberworm\CSS\Value\URL;
|
||||
use Sabberworm\CSS\Value\Value;
|
||||
|
||||
/**
|
||||
* Declaration blocks are the parts of a CSS file which denote the rules belonging to a selector.
|
||||
*
|
||||
* 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
|
||||
*/
|
||||
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
|
||||
*/
|
||||
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
|
||||
*/
|
||||
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
|
||||
*/
|
||||
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
|
||||
*/
|
||||
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
|
||||
*/
|
||||
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
|
||||
*/
|
||||
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, '%', null, false, $this->iLineNo),
|
||||
new Size(0, '%', null, false, $this->iLineNo),
|
||||
],
|
||||
];
|
||||
$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
|
||||
*/
|
||||
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
|
||||
*/
|
||||
public function createShorthandProperties(array $aProperties, $sShorthand)
|
||||
{
|
||||
$aRules = $this->getRulesAssoc();
|
||||
$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 (count($aNewValues)) {
|
||||
$oNewRule = new Rule($sShorthand, $oRule->getLineNo(), $oRule->getColNo());
|
||||
foreach ($aNewValues as $mValue) {
|
||||
$oNewRule->addValue($mValue);
|
||||
}
|
||||
$this->addRule($oNewRule);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function createBackgroundShorthand()
|
||||
{
|
||||
$aProperties = [
|
||||
'background-color',
|
||||
'background-image',
|
||||
'background-repeat',
|
||||
'background-position',
|
||||
'background-attachment',
|
||||
];
|
||||
$this->createShorthandProperties($aProperties, 'background');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
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
|
||||
*/
|
||||
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
|
||||
*/
|
||||
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
|
||||
*/
|
||||
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->iLineNo);
|
||||
$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->iLineNo);
|
||||
$oFFValue->setListComponents($aFFValues);
|
||||
$oNewRule->addValue($oFFValue);
|
||||
|
||||
$this->addRule($oNewRule);
|
||||
foreach ($aFontProperties as $sProperty) {
|
||||
$this->removeRule($sProperty);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*
|
||||
* @throws OutputException
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
return $this->render(new OutputFormat());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*
|
||||
* @throws OutputException
|
||||
*/
|
||||
public function render(OutputFormat $oOutputFormat)
|
||||
{
|
||||
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->iLineNo);
|
||||
}
|
||||
$sResult = $oOutputFormat->sBeforeDeclarationBlock;
|
||||
$sResult .= $oOutputFormat->implode(
|
||||
$oOutputFormat->spaceBeforeSelectorSeparator() . ',' . $oOutputFormat->spaceAfterSelectorSeparator(),
|
||||
$this->aSelectors
|
||||
);
|
||||
$sResult .= $oOutputFormat->sAfterDeclarationBlockSelectors;
|
||||
$sResult .= $oOutputFormat->spaceBeforeOpeningBrace() . '{';
|
||||
$sResult .= parent::render($oOutputFormat);
|
||||
$sResult .= '}';
|
||||
$sResult .= $oOutputFormat->sAfterDeclarationBlock;
|
||||
return $sResult;
|
||||
}
|
||||
}
|
||||
326
lib/sabberworm/php-css-parser/src/RuleSet/RuleSet.php
Normal file
326
lib/sabberworm/php-css-parser/src/RuleSet/RuleSet.php
Normal file
@@ -0,0 +1,326 @@
|
||||
<?php
|
||||
|
||||
namespace Sabberworm\CSS\RuleSet;
|
||||
|
||||
use Sabberworm\CSS\Comment\Comment;
|
||||
use Sabberworm\CSS\Comment\Commentable;
|
||||
use Sabberworm\CSS\OutputFormat;
|
||||
use Sabberworm\CSS\Parsing\ParserState;
|
||||
use Sabberworm\CSS\Parsing\UnexpectedEOFException;
|
||||
use Sabberworm\CSS\Parsing\UnexpectedTokenException;
|
||||
use Sabberworm\CSS\Renderable;
|
||||
use Sabberworm\CSS\Rule\Rule;
|
||||
|
||||
/**
|
||||
* RuleSet is a generic superclass denoting rules. The typical example for rule sets are declaration block.
|
||||
* However, unknown At-Rules (like `@font-face`) are also rule sets.
|
||||
*/
|
||||
abstract class RuleSet implements Renderable, Commentable
|
||||
{
|
||||
/**
|
||||
* @var array<string, Rule>
|
||||
*/
|
||||
private $aRules;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
protected $iLineNo;
|
||||
|
||||
/**
|
||||
* @var array<array-key, Comment>
|
||||
*/
|
||||
protected $aComments;
|
||||
|
||||
/**
|
||||
* @param int $iLineNo
|
||||
*/
|
||||
public function __construct($iLineNo = 0)
|
||||
{
|
||||
$this->aRules = [];
|
||||
$this->iLineNo = $iLineNo;
|
||||
$this->aComments = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*
|
||||
* @throws UnexpectedTokenException
|
||||
* @throws UnexpectedEOFException
|
||||
*/
|
||||
public static function parseRuleSet(ParserState $oParserState, RuleSet $oRuleSet)
|
||||
{
|
||||
while ($oParserState->comes(';')) {
|
||||
$oParserState->consume(';');
|
||||
}
|
||||
while (!$oParserState->comes('}')) {
|
||||
$oRule = null;
|
||||
if ($oParserState->getSettings()->bLenientParsing) {
|
||||
try {
|
||||
$oRule = Rule::parse($oParserState);
|
||||
} 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);
|
||||
}
|
||||
if ($oRule) {
|
||||
$oRuleSet->addRule($oRule);
|
||||
}
|
||||
}
|
||||
$oParserState->consume('}');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getLineNo()
|
||||
{
|
||||
return $this->iLineNo;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Rule|null $oSibling
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function addRule(Rule $oRule, Rule $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->getLineNo() === 0 && $oRule->getColNo() === 0) {
|
||||
//this node is added manually, give it the next best line
|
||||
$rules = $this->getRules();
|
||||
$pos = count($rules);
|
||||
if ($pos > 0) {
|
||||
$last = $rules[$pos - 1];
|
||||
$oRule->setPosition($last->getLineNo() + 1, 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 behaves like calling `getRules($mRule->getRule())`.
|
||||
*
|
||||
* @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 behaves like calling `getRules($mRule->getRule())`.
|
||||
*
|
||||
* @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. This accepts all the possible values that `getRules()` accepts.
|
||||
*
|
||||
* If given a Rule, it will only remove this particular rule (by identity).
|
||||
* If given a name, it will remove all rules by that name.
|
||||
*
|
||||
* Note: this is different from pre-v.2.0 behaviour of PHP-CSS-Parser, where passing a Rule instance would
|
||||
* remove all rules with the same name. To get the old behaviour, use `removeRule($oRule->getRule())`.
|
||||
*
|
||||
* @param Rule|string|null $mRule
|
||||
* pattern to remove. If $mRule is null, all rules are removed. 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. Passing a Rule behaves matches by identity.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
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]);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
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 or equals it
|
||||
// (without the trailing dash).
|
||||
if (
|
||||
!$mRule || $sName === $mRule
|
||||
|| (strrpos($mRule, '-') === strlen($mRule) - strlen('-')
|
||||
&& (strpos($sName, $mRule) === 0 || $sName === substr($mRule, 0, -1)))
|
||||
) {
|
||||
unset($this->aRules[$sName]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
return $this->render(new OutputFormat());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function render(OutputFormat $oOutputFormat)
|
||||
{
|
||||
$sResult = '';
|
||||
$bIsFirst = true;
|
||||
foreach ($this->aRules as $aRules) {
|
||||
foreach ($aRules as $oRule) {
|
||||
$sRendered = $oOutputFormat->safely(function () use ($oRule, $oOutputFormat) {
|
||||
return $oRule->render($oOutputFormat->nextLevel());
|
||||
});
|
||||
if ($sRendered === null) {
|
||||
continue;
|
||||
}
|
||||
if ($bIsFirst) {
|
||||
$bIsFirst = false;
|
||||
$sResult .= $oOutputFormat->nextLevel()->spaceBeforeRules();
|
||||
} else {
|
||||
$sResult .= $oOutputFormat->nextLevel()->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;
|
||||
}
|
||||
}
|
||||
89
lib/sabberworm/php-css-parser/src/Settings.php
Normal file
89
lib/sabberworm/php-css-parser/src/Settings.php
Normal file
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
namespace 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
|
||||
*/
|
||||
public $bMultibyteSupport;
|
||||
|
||||
/**
|
||||
* The default charset for the CSS if no `@charset` rule is found. Defaults to utf-8.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $sDefaultCharset = 'utf-8';
|
||||
|
||||
/**
|
||||
* Lenient parsing. When used (which is true by default), the parser will not choke
|
||||
* on unexpected tokens but simply ignore them.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $bLenientParsing = true;
|
||||
|
||||
private function __construct()
|
||||
{
|
||||
$this->bMultibyteSupport = extension_loaded('mbstring');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return self new instance
|
||||
*/
|
||||
public static function create()
|
||||
{
|
||||
return new Settings();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $bMultibyteSupport
|
||||
*
|
||||
* @return self fluent interface
|
||||
*/
|
||||
public function withMultibyteSupport($bMultibyteSupport = true)
|
||||
{
|
||||
$this->bMultibyteSupport = $bMultibyteSupport;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $sDefaultCharset
|
||||
*
|
||||
* @return self fluent interface
|
||||
*/
|
||||
public function withDefaultCharset($sDefaultCharset)
|
||||
{
|
||||
$this->sDefaultCharset = $sDefaultCharset;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $bLenientParsing
|
||||
*
|
||||
* @return self fluent interface
|
||||
*/
|
||||
public function withLenientParsing($bLenientParsing = true)
|
||||
{
|
||||
$this->bLenientParsing = $bLenientParsing;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return self fluent interface
|
||||
*/
|
||||
public function beStrict()
|
||||
{
|
||||
return $this->withLenientParsing(false);
|
||||
}
|
||||
}
|
||||
73
lib/sabberworm/php-css-parser/src/Value/CSSFunction.php
Normal file
73
lib/sabberworm/php-css-parser/src/Value/CSSFunction.php
Normal file
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
namespace Sabberworm\CSS\Value;
|
||||
|
||||
use Sabberworm\CSS\OutputFormat;
|
||||
|
||||
class CSSFunction extends ValueList
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
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->iLineNo = $iLineNo;
|
||||
parent::__construct($aArguments, $sSeparator, $iLineNo);
|
||||
}
|
||||
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
return $this->render(new OutputFormat());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function render(OutputFormat $oOutputFormat)
|
||||
{
|
||||
$aArguments = parent::render($oOutputFormat);
|
||||
return "{$this->sName}({$aArguments})";
|
||||
}
|
||||
}
|
||||
105
lib/sabberworm/php-css-parser/src/Value/CSSString.php
Normal file
105
lib/sabberworm/php-css-parser/src/Value/CSSString.php
Normal file
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
|
||||
namespace Sabberworm\CSS\Value;
|
||||
|
||||
use Sabberworm\CSS\OutputFormat;
|
||||
use Sabberworm\CSS\Parsing\ParserState;
|
||||
use Sabberworm\CSS\Parsing\SourceException;
|
||||
use Sabberworm\CSS\Parsing\UnexpectedEOFException;
|
||||
use Sabberworm\CSS\Parsing\UnexpectedTokenException;
|
||||
|
||||
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
|
||||
*/
|
||||
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
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
return $this->render(new OutputFormat());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function render(OutputFormat $oOutputFormat)
|
||||
{
|
||||
$sString = addslashes($this->sString);
|
||||
$sString = str_replace("\n", '\A', $sString);
|
||||
return $oOutputFormat->getStringQuotingType() . $sString . $oOutputFormat->getStringQuotingType();
|
||||
}
|
||||
}
|
||||
89
lib/sabberworm/php-css-parser/src/Value/CalcFunction.php
Normal file
89
lib/sabberworm/php-css-parser/src/Value/CalcFunction.php
Normal file
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
namespace Sabberworm\CSS\Value;
|
||||
|
||||
use Sabberworm\CSS\Parsing\ParserState;
|
||||
use Sabberworm\CSS\Parsing\UnexpectedEOFException;
|
||||
use Sabberworm\CSS\Parsing\UnexpectedTokenException;
|
||||
|
||||
class CalcFunction extends CSSFunction
|
||||
{
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
const T_OPERAND = 1;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
const T_OPERATOR = 2;
|
||||
|
||||
/**
|
||||
* @return CalcFunction
|
||||
*
|
||||
* @throws UnexpectedTokenException
|
||||
* @throws UnexpectedEOFException
|
||||
*/
|
||||
public static function parse(ParserState $oParserState)
|
||||
{
|
||||
$aOperators = ['+', '-', '*', '/'];
|
||||
$sFunction = trim($oParserState->consumeUntil('(', false, true));
|
||||
$oCalcList = new CalcRuleValueList($oParserState->currentLine());
|
||||
$oList = new RuleValueList(',', $oParserState->currentLine());
|
||||
$iNestingLevel = 0;
|
||||
$iLastComponentType = null;
|
||||
while (!$oParserState->comes(')') || $iNestingLevel > 0) {
|
||||
$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),
|
||||
$oVal
|
||||
),
|
||||
'',
|
||||
'custom',
|
||||
$oParserState->currentLine()
|
||||
);
|
||||
}
|
||||
}
|
||||
$oParserState->consumeWhiteSpace();
|
||||
}
|
||||
$oList->addListComponent($oCalcList);
|
||||
$oParserState->consume(')');
|
||||
return new CalcFunction($sFunction, $oList, ',', $oParserState->currentLine());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace Sabberworm\CSS\Value;
|
||||
|
||||
use Sabberworm\CSS\OutputFormat;
|
||||
|
||||
class CalcRuleValueList extends RuleValueList
|
||||
{
|
||||
/**
|
||||
* @param int $iLineNo
|
||||
*/
|
||||
public function __construct($iLineNo = 0)
|
||||
{
|
||||
parent::__construct(',', $iLineNo);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function render(OutputFormat $oOutputFormat)
|
||||
{
|
||||
return $oOutputFormat->implode(' ', $this->aComponents);
|
||||
}
|
||||
}
|
||||
166
lib/sabberworm/php-css-parser/src/Value/Color.php
Normal file
166
lib/sabberworm/php-css-parser/src/Value/Color.php
Normal file
@@ -0,0 +1,166 @@
|
||||
<?php
|
||||
|
||||
namespace Sabberworm\CSS\Value;
|
||||
|
||||
use Sabberworm\CSS\OutputFormat;
|
||||
use Sabberworm\CSS\Parsing\ParserState;
|
||||
use Sabberworm\CSS\Parsing\UnexpectedEOFException;
|
||||
use Sabberworm\CSS\Parsing\UnexpectedTokenException;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Color|CSSFunction
|
||||
*
|
||||
* @throws UnexpectedEOFException
|
||||
* @throws UnexpectedTokenException
|
||||
*/
|
||||
public static function parse(ParserState $oParserState)
|
||||
{
|
||||
$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()
|
||||
),
|
||||
];
|
||||
} else {
|
||||
$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 {
|
||||
$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
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
return $this->render(new OutputFormat());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function render(OutputFormat $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);
|
||||
}
|
||||
}
|
||||
65
lib/sabberworm/php-css-parser/src/Value/LineName.php
Normal file
65
lib/sabberworm/php-css-parser/src/Value/LineName.php
Normal file
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace Sabberworm\CSS\Value;
|
||||
|
||||
use Sabberworm\CSS\OutputFormat;
|
||||
use Sabberworm\CSS\Parsing\ParserState;
|
||||
use Sabberworm\CSS\Parsing\UnexpectedEOFException;
|
||||
use 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
|
||||
*/
|
||||
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
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
return $this->render(new OutputFormat());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function render(OutputFormat $oOutputFormat)
|
||||
{
|
||||
return '[' . parent::render(OutputFormat::createCompact()) . ']';
|
||||
}
|
||||
}
|
||||
14
lib/sabberworm/php-css-parser/src/Value/PrimitiveValue.php
Normal file
14
lib/sabberworm/php-css-parser/src/Value/PrimitiveValue.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace Sabberworm\CSS\Value;
|
||||
|
||||
abstract class PrimitiveValue extends Value
|
||||
{
|
||||
/**
|
||||
* @param int $iLineNo
|
||||
*/
|
||||
public function __construct($iLineNo = 0)
|
||||
{
|
||||
parent::__construct($iLineNo);
|
||||
}
|
||||
}
|
||||
15
lib/sabberworm/php-css-parser/src/Value/RuleValueList.php
Normal file
15
lib/sabberworm/php-css-parser/src/Value/RuleValueList.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace Sabberworm\CSS\Value;
|
||||
|
||||
class RuleValueList extends ValueList
|
||||
{
|
||||
/**
|
||||
* @param string $sSeparator
|
||||
* @param int $iLineNo
|
||||
*/
|
||||
public function __construct($sSeparator = ',', $iLineNo = 0)
|
||||
{
|
||||
parent::__construct([], $sSeparator, $iLineNo);
|
||||
}
|
||||
}
|
||||
209
lib/sabberworm/php-css-parser/src/Value/Size.php
Normal file
209
lib/sabberworm/php-css-parser/src/Value/Size.php
Normal file
@@ -0,0 +1,209 @@
|
||||
<?php
|
||||
|
||||
namespace Sabberworm\CSS\Value;
|
||||
|
||||
use Sabberworm\CSS\OutputFormat;
|
||||
use Sabberworm\CSS\Parsing\ParserState;
|
||||
use Sabberworm\CSS\Parsing\UnexpectedEOFException;
|
||||
use Sabberworm\CSS\Parsing\UnexpectedTokenException;
|
||||
|
||||
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>
|
||||
*/
|
||||
const ABSOLUTE_SIZE_UNITS = ['px', 'cm', 'mm', 'mozmm', 'in', 'pt', 'pc', 'vh', 'vw', 'vmin', 'vmax', 'rem'];
|
||||
|
||||
/**
|
||||
* @var array<int, string>
|
||||
*/
|
||||
const RELATIVE_SIZE_UNITS = ['%', 'em', 'ex', 'ch', 'fr'];
|
||||
|
||||
/**
|
||||
* @var array<int, string>
|
||||
*/
|
||||
const NON_SIZE_UNITS = ['deg', 'grad', 'rad', 's', 'ms', 'turns', '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
|
||||
*/
|
||||
public static function parse(ParserState $oParserState, $bIsColorComponent = false)
|
||||
{
|
||||
$sSize = '';
|
||||
if ($oParserState->comes('-')) {
|
||||
$sSize .= $oParserState->consume('-');
|
||||
}
|
||||
while (is_numeric($oParserState->peek()) || $oParserState->comes('.')) {
|
||||
if ($oParserState->comes('.')) {
|
||||
$sSize .= $oParserState->consume('.');
|
||||
} 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
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
return $this->render(new OutputFormat());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function render(OutputFormat $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)) : $this->fSize;
|
||||
return preg_replace(["/$sPoint/", "/^(-?)0\./"], ['.', '$1.'], $sSize)
|
||||
. ($this->sUnit === null ? '' : $this->sUnit);
|
||||
}
|
||||
}
|
||||
82
lib/sabberworm/php-css-parser/src/Value/URL.php
Normal file
82
lib/sabberworm/php-css-parser/src/Value/URL.php
Normal file
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
namespace Sabberworm\CSS\Value;
|
||||
|
||||
use Sabberworm\CSS\OutputFormat;
|
||||
use Sabberworm\CSS\Parsing\ParserState;
|
||||
use Sabberworm\CSS\Parsing\SourceException;
|
||||
use Sabberworm\CSS\Parsing\UnexpectedEOFException;
|
||||
use Sabberworm\CSS\Parsing\UnexpectedTokenException;
|
||||
|
||||
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
|
||||
*/
|
||||
public static function parse(ParserState $oParserState)
|
||||
{
|
||||
$bUseUrl = $oParserState->comes('url', true);
|
||||
if ($bUseUrl) {
|
||||
$oParserState->consume('url');
|
||||
$oParserState->consumeWhiteSpace();
|
||||
$oParserState->consume('(');
|
||||
}
|
||||
$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
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
return $this->render(new OutputFormat());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function render(OutputFormat $oOutputFormat)
|
||||
{
|
||||
return "url({$this->oURL->render($oOutputFormat)})";
|
||||
}
|
||||
}
|
||||
198
lib/sabberworm/php-css-parser/src/Value/Value.php
Normal file
198
lib/sabberworm/php-css-parser/src/Value/Value.php
Normal file
@@ -0,0 +1,198 @@
|
||||
<?php
|
||||
|
||||
namespace Sabberworm\CSS\Value;
|
||||
|
||||
use Sabberworm\CSS\Parsing\ParserState;
|
||||
use Sabberworm\CSS\Parsing\SourceException;
|
||||
use Sabberworm\CSS\Parsing\UnexpectedEOFException;
|
||||
use Sabberworm\CSS\Parsing\UnexpectedTokenException;
|
||||
use Sabberworm\CSS\Renderable;
|
||||
|
||||
abstract class Value implements Renderable
|
||||
{
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
protected $iLineNo;
|
||||
|
||||
/**
|
||||
* @param int $iLineNo
|
||||
*/
|
||||
public function __construct($iLineNo = 0)
|
||||
{
|
||||
$this->iLineNo = $iLineNo;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<array-key, string> $aListDelimiters
|
||||
*
|
||||
* @return RuleValueList|CSSFunction|CSSString|LineName|Size|URL|string
|
||||
*
|
||||
* @throws UnexpectedTokenException
|
||||
* @throws UnexpectedEOFException
|
||||
*/
|
||||
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('\\'))
|
||||
) {
|
||||
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) {
|
||||
if (count($aStack) === 1) {
|
||||
return $aStack[0];
|
||||
}
|
||||
$iStartPosition = null;
|
||||
while (($iStartPosition = array_search($sDelimiter, $aStack, true)) !== false) {
|
||||
$iLength = 2; //Number of elements to be joined
|
||||
for ($i = $iStartPosition + 2; $i < count($aStack); $i += 2, ++$iLength) {
|
||||
if ($sDelimiter !== $aStack[$i]) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
$oList = new RuleValueList($sDelimiter, $oParserState->currentLine());
|
||||
for ($i = $iStartPosition - 1; $i - $iStartPosition + 1 < $iLength * 2; $i += 2) {
|
||||
$oList->addListComponent($aStack[$i]);
|
||||
}
|
||||
array_splice($aStack, $iStartPosition - 1, $iLength * 2 - 1, [$oList]);
|
||||
}
|
||||
}
|
||||
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
|
||||
*/
|
||||
public static function parseIdentifierOrFunction(ParserState $oParserState, $bIgnoreCase = false)
|
||||
{
|
||||
$sResult = $oParserState->parseIdentifier($bIgnoreCase);
|
||||
|
||||
if ($oParserState->comes('(')) {
|
||||
$oParserState->consume('(');
|
||||
$aArguments = Value::parseValue($oParserState, ['=', ' ', ',']);
|
||||
$sResult = new CSSFunction($sResult, $aArguments, ',', $oParserState->currentLine());
|
||||
$oParserState->consume(')');
|
||||
}
|
||||
|
||||
return $sResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return CSSFunction|CSSString|LineName|Size|URL|string
|
||||
*
|
||||
* @throws UnexpectedEOFException
|
||||
* @throws UnexpectedTokenException
|
||||
* @throws SourceException
|
||||
*/
|
||||
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('url', true)) {
|
||||
$oValue = URL::parse($oParserState);
|
||||
} elseif (
|
||||
$oParserState->comes('calc', true) || $oParserState->comes('-webkit-calc', true)
|
||||
|| $oParserState->comes('-moz-calc', true)
|
||||
) {
|
||||
$oValue = CalcFunction::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 {
|
||||
$oValue = self::parseIdentifierOrFunction($oParserState);
|
||||
}
|
||||
$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}";
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getLineNo()
|
||||
{
|
||||
return $this->iLineNo;
|
||||
}
|
||||
}
|
||||
100
lib/sabberworm/php-css-parser/src/Value/ValueList.php
Normal file
100
lib/sabberworm/php-css-parser/src/Value/ValueList.php
Normal file
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
|
||||
namespace Sabberworm\CSS\Value;
|
||||
|
||||
use Sabberworm\CSS\OutputFormat;
|
||||
|
||||
abstract class ValueList extends Value
|
||||
{
|
||||
/**
|
||||
* @var array<int, RuleValueList|CSSFunction|CSSString|LineName|Size|URL|string>
|
||||
*/
|
||||
protected $aComponents;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
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
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
return $this->render(new OutputFormat());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function render(OutputFormat $oOutputFormat)
|
||||
{
|
||||
return $oOutputFormat->implode(
|
||||
$oOutputFormat->spaceBeforeListArgumentSeparator($this->sSeparator) . $this->sSeparator
|
||||
. $oOutputFormat->spaceAfterListArgumentSeparator($this->sSeparator),
|
||||
$this->aComponents
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user