tipo: modificación del cpanel y gitignore de la carpeta vendor
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sabberworm\CSS\Property;
|
||||
|
||||
use Sabberworm\CSS\CSSList\CSSListItem;
|
||||
|
||||
/**
|
||||
* Note that `CSSListItem` extends both `Commentable` and `Renderable`,
|
||||
* so concrete classes implementing this interface must also implement those.
|
||||
*/
|
||||
interface AtRule extends CSSListItem
|
||||
{
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @internal since 8.5.2
|
||||
*/
|
||||
public const BLOCK_RULES = [
|
||||
'media',
|
||||
'document',
|
||||
'supports',
|
||||
'region-style',
|
||||
'font-feature-values',
|
||||
'container',
|
||||
'layer',
|
||||
'scope',
|
||||
'starting-style',
|
||||
];
|
||||
|
||||
/**
|
||||
* @return non-empty-string
|
||||
*/
|
||||
public function atRuleName(): string;
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sabberworm\CSS\Property;
|
||||
|
||||
use Sabberworm\CSS\Comment\CommentContainer;
|
||||
use Sabberworm\CSS\OutputFormat;
|
||||
use Sabberworm\CSS\Position\Position;
|
||||
use Sabberworm\CSS\Position\Positionable;
|
||||
use Sabberworm\CSS\ShortClassNameProvider;
|
||||
use Sabberworm\CSS\Value\CSSString;
|
||||
use Sabberworm\CSS\Value\URL;
|
||||
|
||||
/**
|
||||
* `CSSNamespace` represents an `@namespace` rule.
|
||||
*/
|
||||
class CSSNamespace implements AtRule, Positionable
|
||||
{
|
||||
use CommentContainer;
|
||||
use Position;
|
||||
use ShortClassNameProvider;
|
||||
|
||||
/**
|
||||
* @var CSSString|URL
|
||||
*/
|
||||
private $url;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
private $prefix;
|
||||
|
||||
/**
|
||||
* @param CSSString|URL $url
|
||||
* @param int<1, max>|null $lineNumber
|
||||
*/
|
||||
public function __construct($url, ?string $prefix = null, ?int $lineNumber = null)
|
||||
{
|
||||
$this->url = $url;
|
||||
$this->prefix = $prefix;
|
||||
$this->setPosition($lineNumber);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return non-empty-string
|
||||
*/
|
||||
public function render(OutputFormat $outputFormat): string
|
||||
{
|
||||
return '@namespace ' . ($this->prefix === null ? '' : $this->prefix . ' ')
|
||||
. $this->url->render($outputFormat) . ';';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return CSSString|URL
|
||||
*/
|
||||
public function getUrl()
|
||||
{
|
||||
return $this->url;
|
||||
}
|
||||
|
||||
public function getPrefix(): ?string
|
||||
{
|
||||
return $this->prefix;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param CSSString|URL $url
|
||||
*/
|
||||
public function setUrl($url): void
|
||||
{
|
||||
$this->url = $url;
|
||||
}
|
||||
|
||||
public function setPrefix(string $prefix): void
|
||||
{
|
||||
$this->prefix = $prefix;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return non-empty-string
|
||||
*/
|
||||
public function atRuleName(): string
|
||||
{
|
||||
return 'namespace';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{0: CSSString|URL|non-empty-string, 1?: CSSString|URL}
|
||||
*/
|
||||
public function atRuleArgs(): array
|
||||
{
|
||||
$result = [$this->url];
|
||||
if (\is_string($this->prefix) && $this->prefix !== '') {
|
||||
\array_unshift($result, $this->prefix);
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, bool|int|float|string|array<mixed>|null>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public function getArrayRepresentation(): array
|
||||
{
|
||||
return [
|
||||
'class' => $this->getShortClassName(),
|
||||
// We're using `uri` here instead of `url` to better match the spec.
|
||||
'uri' => $this->url->getArrayRepresentation(),
|
||||
'prefix' => $this->prefix,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sabberworm\CSS\Property;
|
||||
|
||||
use Sabberworm\CSS\Comment\CommentContainer;
|
||||
use Sabberworm\CSS\OutputFormat;
|
||||
use Sabberworm\CSS\Position\Position;
|
||||
use Sabberworm\CSS\Position\Positionable;
|
||||
use Sabberworm\CSS\ShortClassNameProvider;
|
||||
use Sabberworm\CSS\Value\CSSString;
|
||||
|
||||
/**
|
||||
* Class representing an `@charset` rule.
|
||||
*
|
||||
* The following restrictions apply:
|
||||
* - May not be found in any CSSList other than the Document.
|
||||
* - May only appear at the very top of a Document’s contents.
|
||||
* - Must not appear more than once.
|
||||
*/
|
||||
class Charset implements AtRule, Positionable
|
||||
{
|
||||
use CommentContainer;
|
||||
use Position;
|
||||
use ShortClassNameProvider;
|
||||
|
||||
/**
|
||||
* @var CSSString
|
||||
*/
|
||||
private $charset;
|
||||
|
||||
/**
|
||||
* @param int<1, max>|null $lineNumber
|
||||
*/
|
||||
public function __construct(CSSString $charset, ?int $lineNumber = null)
|
||||
{
|
||||
$this->charset = $charset;
|
||||
$this->setPosition($lineNumber);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|CSSString $charset
|
||||
*/
|
||||
public function setCharset($charset): void
|
||||
{
|
||||
$charset = $charset instanceof CSSString ? $charset : new CSSString($charset);
|
||||
$this->charset = $charset;
|
||||
}
|
||||
|
||||
public function getCharset(): string
|
||||
{
|
||||
return $this->charset->getString();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return non-empty-string
|
||||
*/
|
||||
public function render(OutputFormat $outputFormat): string
|
||||
{
|
||||
return "{$outputFormat->getFormatter()->comments($this)}@charset {$this->charset->render($outputFormat)};";
|
||||
}
|
||||
|
||||
/**
|
||||
* @return non-empty-string
|
||||
*/
|
||||
public function atRuleName(): string
|
||||
{
|
||||
return 'charset';
|
||||
}
|
||||
|
||||
public function atRuleArgs(): CSSString
|
||||
{
|
||||
return $this->charset;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, bool|int|float|string|array<mixed>|null>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public function getArrayRepresentation(): array
|
||||
{
|
||||
return [
|
||||
'class' => $this->getShortClassName(),
|
||||
'charset' => $this->charset->getArrayRepresentation(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sabberworm\CSS\Property;
|
||||
|
||||
use Sabberworm\CSS\Comment\Comment;
|
||||
use Sabberworm\CSS\Comment\Commentable;
|
||||
use Sabberworm\CSS\Comment\CommentContainer;
|
||||
use Sabberworm\CSS\CSSElement;
|
||||
use Sabberworm\CSS\OutputFormat;
|
||||
use Sabberworm\CSS\Parsing\ParserState;
|
||||
use Sabberworm\CSS\Parsing\UnexpectedEOFException;
|
||||
use Sabberworm\CSS\Parsing\UnexpectedTokenException;
|
||||
use Sabberworm\CSS\Position\Position;
|
||||
use Sabberworm\CSS\Position\Positionable;
|
||||
use Sabberworm\CSS\Value\RuleValueList;
|
||||
use Sabberworm\CSS\Value\Value;
|
||||
|
||||
use function Safe\preg_match;
|
||||
|
||||
/**
|
||||
* `Declaration`s just have a string key (the property name) and a 'Value'.
|
||||
*
|
||||
* In CSS, `Declaration`s are expressed as follows: “key: value[0][0] value[0][1], value[1][0] value[1][1];”
|
||||
*/
|
||||
class Declaration implements Commentable, CSSElement, Positionable
|
||||
{
|
||||
use CommentContainer;
|
||||
use Position;
|
||||
|
||||
/**
|
||||
* @var non-empty-string
|
||||
*/
|
||||
private $propertyName;
|
||||
|
||||
/**
|
||||
* @var RuleValueList|string|null
|
||||
*/
|
||||
private $value;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $isImportant = false;
|
||||
|
||||
/**
|
||||
* @param non-empty-string $propertyName
|
||||
* @param int<1, max>|null $lineNumber
|
||||
* @param int<0, max>|null $columnNumber
|
||||
*/
|
||||
public function __construct(string $propertyName, ?int $lineNumber = null, ?int $columnNumber = null)
|
||||
{
|
||||
$this->propertyName = $propertyName;
|
||||
$this->setPosition($lineNumber, $columnNumber);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<Comment> $commentsBefore
|
||||
*
|
||||
* @throws UnexpectedEOFException
|
||||
* @throws UnexpectedTokenException
|
||||
*
|
||||
* @internal since V8.8.0
|
||||
*/
|
||||
public static function parse(ParserState $parserState, array $commentsBefore = []): self
|
||||
{
|
||||
$comments = $commentsBefore;
|
||||
$parserState->consumeWhiteSpace($comments);
|
||||
$declaration = new self(
|
||||
$parserState->parseIdentifier(!$parserState->comes('--')),
|
||||
$parserState->currentLine(),
|
||||
$parserState->currentColumn()
|
||||
);
|
||||
$parserState->consumeWhiteSpace($comments);
|
||||
$declaration->setComments($comments);
|
||||
$parserState->consume(':');
|
||||
$value = Value::parseValue($parserState, self::getDelimitersForPropertyValue($declaration->getPropertyName()));
|
||||
$declaration->setValue($value);
|
||||
$parserState->consumeWhiteSpace();
|
||||
if ($parserState->comes('!')) {
|
||||
$parserState->consume('!');
|
||||
$parserState->consumeWhiteSpace();
|
||||
$parserState->consume('important');
|
||||
$declaration->setIsImportant(true);
|
||||
}
|
||||
$parserState->consumeWhiteSpace();
|
||||
while ($parserState->comes(';')) {
|
||||
$parserState->consume(';');
|
||||
}
|
||||
|
||||
return $declaration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of delimiters (or separators).
|
||||
* The first item is the innermost separator (or, put another way, the highest-precedence operator).
|
||||
* The sequence continues to the outermost separator (or lowest-precedence operator).
|
||||
*
|
||||
* @param non-empty-string $propertyName
|
||||
*
|
||||
* @return list<non-empty-string>
|
||||
*/
|
||||
private static function getDelimitersForPropertyValue(string $propertyName): array
|
||||
{
|
||||
if (preg_match('/^font($|-)/', $propertyName) === 1) {
|
||||
return [',', '/', ' '];
|
||||
}
|
||||
|
||||
switch ($propertyName) {
|
||||
case 'src':
|
||||
return [' ', ','];
|
||||
default:
|
||||
return [',', ' ', '/'];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param non-empty-string $propertyName
|
||||
*/
|
||||
public function setPropertyName(string $propertyName): void
|
||||
{
|
||||
$this->propertyName = $propertyName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return non-empty-string
|
||||
*/
|
||||
public function getPropertyName(): string
|
||||
{
|
||||
return $this->propertyName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param non-empty-string $propertyName
|
||||
*
|
||||
* @deprecated in v9.2, will be removed in v10.0; use `setPropertyName()` instead.
|
||||
*/
|
||||
public function setRule(string $propertyName): void
|
||||
{
|
||||
$this->propertyName = $propertyName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return non-empty-string
|
||||
*
|
||||
* @deprecated in v9.2, will be removed in v10.0; use `getPropertyName()` instead.
|
||||
*/
|
||||
public function getRule(): string
|
||||
{
|
||||
return $this->propertyName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return RuleValueList|string|null
|
||||
*/
|
||||
public function getValue()
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param RuleValueList|string|null $value
|
||||
*/
|
||||
public function setValue($value): void
|
||||
{
|
||||
$this->value = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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> $value
|
||||
*/
|
||||
public function addValue($value, string $type = ' '): void
|
||||
{
|
||||
if (!\is_array($value)) {
|
||||
$value = [$value];
|
||||
}
|
||||
if (!($this->value instanceof RuleValueList) || $this->value->getListSeparator() !== $type) {
|
||||
$currentValue = $this->value;
|
||||
$this->value = new RuleValueList($type, $this->getLineNumber());
|
||||
if ($currentValue !== null && $currentValue !== '') {
|
||||
$this->value->addListComponent($currentValue);
|
||||
}
|
||||
}
|
||||
foreach ($value as $valueItem) {
|
||||
$this->value->addListComponent($valueItem);
|
||||
}
|
||||
}
|
||||
|
||||
public function setIsImportant(bool $isImportant): void
|
||||
{
|
||||
$this->isImportant = $isImportant;
|
||||
}
|
||||
|
||||
public function getIsImportant(): bool
|
||||
{
|
||||
return $this->isImportant;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return non-empty-string
|
||||
*/
|
||||
public function render(OutputFormat $outputFormat): string
|
||||
{
|
||||
$formatter = $outputFormat->getFormatter();
|
||||
$result = "{$formatter->comments($this)}{$this->propertyName}:{$formatter->spaceAfterRuleName()}";
|
||||
if ($this->value instanceof Value) { // Can also be a ValueList
|
||||
$result .= $this->value->render($outputFormat);
|
||||
} else {
|
||||
$result .= $this->value;
|
||||
}
|
||||
if ($this->isImportant) {
|
||||
$result .= ' !important';
|
||||
}
|
||||
$result .= ';';
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, bool|int|float|string|array<mixed>|null>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public function getArrayRepresentation(): array
|
||||
{
|
||||
throw new \BadMethodCallException('`getArrayRepresentation` is not yet implemented for `' . self::class . '`');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sabberworm\CSS\Property;
|
||||
|
||||
use Sabberworm\CSS\Comment\CommentContainer;
|
||||
use Sabberworm\CSS\OutputFormat;
|
||||
use Sabberworm\CSS\Position\Position;
|
||||
use Sabberworm\CSS\Position\Positionable;
|
||||
use Sabberworm\CSS\ShortClassNameProvider;
|
||||
use Sabberworm\CSS\Value\URL;
|
||||
|
||||
/**
|
||||
* Class representing an `@import` rule.
|
||||
*/
|
||||
class Import implements AtRule, Positionable
|
||||
{
|
||||
use CommentContainer;
|
||||
use Position;
|
||||
use ShortClassNameProvider;
|
||||
|
||||
/**
|
||||
* @var URL
|
||||
*/
|
||||
private $location;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
private $mediaQuery;
|
||||
|
||||
/**
|
||||
* @param int<1, max>|null $lineNumber
|
||||
*/
|
||||
public function __construct(URL $location, ?string $mediaQuery, ?int $lineNumber = null)
|
||||
{
|
||||
$this->location = $location;
|
||||
$this->mediaQuery = $mediaQuery;
|
||||
$this->setPosition($lineNumber);
|
||||
}
|
||||
|
||||
public function setLocation(URL $location): void
|
||||
{
|
||||
$this->location = $location;
|
||||
}
|
||||
|
||||
public function getLocation(): URL
|
||||
{
|
||||
return $this->location;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return non-empty-string
|
||||
*/
|
||||
public function render(OutputFormat $outputFormat): string
|
||||
{
|
||||
return $outputFormat->getFormatter()->comments($this) . '@import ' . $this->location->render($outputFormat)
|
||||
. ($this->mediaQuery === null ? '' : ' ' . $this->mediaQuery) . ';';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return non-empty-string
|
||||
*/
|
||||
public function atRuleName(): string
|
||||
{
|
||||
return 'import';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{0: URL, 1?: non-empty-string}
|
||||
*/
|
||||
public function atRuleArgs(): array
|
||||
{
|
||||
$result = [$this->location];
|
||||
if (\is_string($this->mediaQuery) && $this->mediaQuery !== '') {
|
||||
$result[] = $this->mediaQuery;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function getMediaQuery(): ?string
|
||||
{
|
||||
return $this->mediaQuery;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, bool|int|float|string|array<mixed>|null>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public function getArrayRepresentation(): array
|
||||
{
|
||||
return [
|
||||
'class' => $this->getShortClassName(),
|
||||
// We're using the term "uri" here to match the wording used in the specs:
|
||||
// https://www.w3.org/TR/CSS22/cascade.html#at-import
|
||||
'uri' => $this->location->getArrayRepresentation(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sabberworm\CSS\Property;
|
||||
|
||||
class KeyframeSelector extends Selector
|
||||
{
|
||||
/**
|
||||
* This differs from the parent class:
|
||||
* - comma is not allowed unless escaped or quoted;
|
||||
* - percentage value is allowed by itself.
|
||||
*
|
||||
* @internal since 8.5.2
|
||||
*/
|
||||
public const SELECTOR_VALIDATION_RX = '/
|
||||
^(
|
||||
(?:
|
||||
# any sequence of valid unescaped characters, except quotes
|
||||
[a-zA-Z0-9\\x{00A0}-\\x{FFFF}_^$|*=~\\[\\]()\\-\\s\\.:#+>]++
|
||||
|
|
||||
# one or more escaped characters
|
||||
(?:\\\\.)++
|
||||
|
|
||||
# quoted text, like in `[id="example"]`
|
||||
(?:
|
||||
# opening quote
|
||||
([\'"])
|
||||
(?:
|
||||
# sequence of characters except closing quote or backslash
|
||||
(?:(?!\\g{-1}|\\\\).)++
|
||||
|
|
||||
# one or more escaped characters
|
||||
(?:\\\\.)++
|
||||
)*+ # zero or more times
|
||||
# closing quote or end (unmatched quote is currently allowed)
|
||||
(?:\\g{-1}|$)
|
||||
)
|
||||
)*+ # zero or more times
|
||||
|
|
||||
# keyframe animation progress percentage (e.g. 50%), untrimmed
|
||||
\\s*+(\\d++%)\\s*+
|
||||
)$
|
||||
/ux';
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sabberworm\CSS\Property;
|
||||
|
||||
use Sabberworm\CSS\Comment\Comment;
|
||||
use Sabberworm\CSS\OutputFormat;
|
||||
use Sabberworm\CSS\Parsing\ParserState;
|
||||
use Sabberworm\CSS\Parsing\UnexpectedTokenException;
|
||||
use Sabberworm\CSS\Property\Selector\Combinator;
|
||||
use Sabberworm\CSS\Property\Selector\Component;
|
||||
use Sabberworm\CSS\Property\Selector\CompoundSelector;
|
||||
use Sabberworm\CSS\Renderable;
|
||||
use Sabberworm\CSS\Settings;
|
||||
use Sabberworm\CSS\ShortClassNameProvider;
|
||||
|
||||
use function Safe\preg_match;
|
||||
|
||||
/**
|
||||
* Class representing a single CSS selector. Selectors have to be split by the comma prior to being passed into this
|
||||
* class.
|
||||
*/
|
||||
class Selector implements Renderable
|
||||
{
|
||||
use ShortClassNameProvider;
|
||||
|
||||
/**
|
||||
* @internal since 8.5.2
|
||||
*/
|
||||
public const SELECTOR_VALIDATION_RX = '/
|
||||
^(
|
||||
# not whitespace only
|
||||
(?!\\s*+$)
|
||||
(?:
|
||||
# any sequence of valid unescaped characters, except quotes
|
||||
[a-zA-Z0-9\\x{00A0}-\\x{FFFF}_^$|*=~\\[\\]()\\-\\s\\.:#+>,]++
|
||||
|
|
||||
# one or more escaped characters
|
||||
(?:\\\\.)++
|
||||
|
|
||||
# quoted text, like in `[id="example"]`
|
||||
(?:
|
||||
# opening quote
|
||||
([\'"])
|
||||
(?:
|
||||
# sequence of characters except closing quote or backslash
|
||||
(?:(?!\\g{-1}|\\\\).)++
|
||||
|
|
||||
# one or more escaped characters
|
||||
(?:\\\\.)++
|
||||
)*+ # zero or more times
|
||||
# closing quote or end (unmatched quote is currently allowed)
|
||||
(?:\\g{-1}|$)
|
||||
)
|
||||
)*+ # zero or more times
|
||||
)$
|
||||
/ux';
|
||||
|
||||
/**
|
||||
* @var non-empty-list<Component>
|
||||
*/
|
||||
private $components;
|
||||
|
||||
/**
|
||||
* @internal since V8.8.0
|
||||
*/
|
||||
public static function isValid(string $selector): bool
|
||||
{
|
||||
// Note: We need to use `static::` here as the constant is overridden in the `KeyframeSelector` class.
|
||||
$numberOfMatches = preg_match(static::SELECTOR_VALIDATION_RX, $selector);
|
||||
|
||||
return $numberOfMatches === 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param non-empty-string|non-empty-list<Component> $selector
|
||||
* Providing a string is deprecated in version 9.2 and will not work from v10.0
|
||||
*
|
||||
* @throws UnexpectedTokenException if the selector is not valid
|
||||
*/
|
||||
final public function __construct($selector)
|
||||
{
|
||||
if (\is_string($selector)) {
|
||||
$this->setSelector($selector);
|
||||
} else {
|
||||
$this->setComponents($selector);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<Comment> $comments
|
||||
*
|
||||
* @return non-empty-list<Component>
|
||||
*
|
||||
* @throws UnexpectedTokenException
|
||||
*/
|
||||
private static function parseComponents(ParserState $parserState, array &$comments = []): array
|
||||
{
|
||||
// Whitespace is a descendent combinator, not allowed around a compound selector.
|
||||
// (It is allowed within, e.g. as part of a string or within a function like `:not()`.)
|
||||
// Gobble any up now to get a clean start.
|
||||
$parserState->consumeWhiteSpace($comments);
|
||||
|
||||
$selectorParts = [];
|
||||
while (true) {
|
||||
try {
|
||||
$selectorParts[] = CompoundSelector::parse($parserState, $comments);
|
||||
} catch (UnexpectedTokenException $e) {
|
||||
if ($selectorParts !== [] && \end($selectorParts)->getValue() === ' ') {
|
||||
// The whitespace was not a descendent combinator, and was, in fact, arbitrary,
|
||||
// after the end of the selector. Discard it.
|
||||
\array_pop($selectorParts);
|
||||
break;
|
||||
} else {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
try {
|
||||
$selectorParts[] = Combinator::parse($parserState, $comments);
|
||||
} catch (UnexpectedTokenException $e) {
|
||||
// End of selector has been reached.
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $selectorParts;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<Comment> $comments
|
||||
*
|
||||
* @throws UnexpectedTokenException
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public static function parse(ParserState $parserState, array &$comments = []): self
|
||||
{
|
||||
$selectorParts = self::parseComponents($parserState, $comments);
|
||||
|
||||
// Check that the selector has been fully parsed:
|
||||
if (!\in_array($parserState->peek(), ['{', '}', ',', ''], true)) {
|
||||
throw new UnexpectedTokenException(
|
||||
'`,`, `{`, `}` or EOF',
|
||||
$parserState->peek(5),
|
||||
'literal',
|
||||
$parserState->currentLine()
|
||||
);
|
||||
}
|
||||
|
||||
return new static($selectorParts);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return non-empty-list<Component>
|
||||
*/
|
||||
public function getComponents(): array
|
||||
{
|
||||
return $this->components;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param non-empty-list<Component> $components
|
||||
* This should be an alternating sequence of `CompoundSelector` and `Combinator`, starting and ending with a
|
||||
* `CompoundSelector`, and may be a single `CompoundSelector`.
|
||||
*/
|
||||
public function setComponents(array $components): self
|
||||
{
|
||||
$this->components = $components;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return non-empty-string
|
||||
*
|
||||
* @deprecated in version 9.2, will be removed in v10.0. Use either `getComponents()` or `render()` instead.
|
||||
*/
|
||||
public function getSelector(): string
|
||||
{
|
||||
return $this->render(new OutputFormat());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param non-empty-string $selector
|
||||
*
|
||||
* @throws UnexpectedTokenException if the selector is not valid
|
||||
*
|
||||
* @deprecated in version 9.2, will be removed in v10.0. Use `setComponents()` instead.
|
||||
*/
|
||||
public function setSelector(string $selector): void
|
||||
{
|
||||
$parserState = new ParserState($selector, Settings::create());
|
||||
|
||||
$components = self::parseComponents($parserState);
|
||||
|
||||
// Check that the selector has been fully parsed:
|
||||
if (!$parserState->isEnd()) {
|
||||
throw new UnexpectedTokenException(
|
||||
'EOF',
|
||||
$parserState->peek(5),
|
||||
'literal'
|
||||
);
|
||||
}
|
||||
|
||||
$this->components = $components;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int<0, max>
|
||||
*/
|
||||
public function getSpecificity(): int
|
||||
{
|
||||
return \array_sum(\array_map(
|
||||
static function (Component $component): int {
|
||||
return $component->getSpecificity();
|
||||
},
|
||||
$this->components
|
||||
));
|
||||
}
|
||||
|
||||
public function render(OutputFormat $outputFormat): string
|
||||
{
|
||||
return \implode('', \array_map(
|
||||
static function (Component $component) use ($outputFormat): string {
|
||||
return $component->render($outputFormat);
|
||||
},
|
||||
$this->components
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, bool|int|float|string|array<mixed>|null>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public function getArrayRepresentation(): array
|
||||
{
|
||||
return [
|
||||
'class' => $this->getShortClassName(),
|
||||
'components' => \array_map(
|
||||
static function (Component $component): array {
|
||||
return $component->getArrayRepresentation();
|
||||
},
|
||||
$this->components
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sabberworm\CSS\Property\Selector;
|
||||
|
||||
use Sabberworm\CSS\Comment\Comment;
|
||||
use Sabberworm\CSS\OutputFormat;
|
||||
use Sabberworm\CSS\Parsing\ParserState;
|
||||
use Sabberworm\CSS\Parsing\UnexpectedTokenException;
|
||||
use Sabberworm\CSS\ShortClassNameProvider;
|
||||
|
||||
/**
|
||||
* Class representing a CSS selector combinator (space, `>`, `+`, or `~`).
|
||||
*
|
||||
* @phpstan-type ValidCombinatorValue ' '|'>'|'+'|'~'
|
||||
*/
|
||||
class Combinator implements Component
|
||||
{
|
||||
use ShortClassNameProvider;
|
||||
|
||||
/**
|
||||
* @var ValidCombinatorValue
|
||||
*/
|
||||
private $value;
|
||||
|
||||
/**
|
||||
* @param ValidCombinatorValue $value
|
||||
*/
|
||||
public function __construct(string $value)
|
||||
{
|
||||
$this->setValue($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<Comment> $comments
|
||||
*
|
||||
* @throws UnexpectedTokenException
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public static function parse(ParserState $parserState, array &$comments = []): self
|
||||
{
|
||||
$consumedWhitespace = $parserState->consumeWhiteSpace($comments);
|
||||
|
||||
$nextToken = $parserState->peek();
|
||||
if (\in_array($nextToken, ['>', '+', '~'], true)) {
|
||||
$value = $nextToken;
|
||||
$parserState->consume(1);
|
||||
$parserState->consumeWhiteSpace($comments);
|
||||
} elseif ($consumedWhitespace !== '') {
|
||||
$value = ' ';
|
||||
} else {
|
||||
throw new UnexpectedTokenException(
|
||||
'combinator',
|
||||
$nextToken,
|
||||
'literal',
|
||||
$parserState->currentLine()
|
||||
);
|
||||
}
|
||||
|
||||
return new self($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ValidCombinatorValue
|
||||
*/
|
||||
public function getValue(): string
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param non-empty-string $value
|
||||
*
|
||||
* @throws \UnexpectedValueException if `$value` is not either space, '>', '+' or '~'
|
||||
*/
|
||||
public function setValue(string $value): void
|
||||
{
|
||||
if (!\in_array($value, [' ', '>', '+', '~'], true)) {
|
||||
throw new \UnexpectedValueException('`' . $value . '` is not a valid selector combinator.');
|
||||
}
|
||||
|
||||
$this->value = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int<0, max>
|
||||
*/
|
||||
public function getSpecificity(): int
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
public function render(OutputFormat $outputFormat): string
|
||||
{
|
||||
$spacing = $outputFormat->getSpaceAroundSelectorCombinator();
|
||||
if ($this->value === ' ') {
|
||||
$rendering = $spacing !== '' ? $spacing : ' ';
|
||||
} else {
|
||||
$rendering = $spacing . $this->value . $spacing;
|
||||
}
|
||||
|
||||
return $rendering;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, bool|int|float|string|array<mixed>|null>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public function getArrayRepresentation(): array
|
||||
{
|
||||
return [
|
||||
'class' => $this->getShortClassName(),
|
||||
'value' => $this->value,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sabberworm\CSS\Property\Selector;
|
||||
|
||||
use Sabberworm\CSS\Renderable;
|
||||
|
||||
/**
|
||||
* This interface is for a class that represents a part of a selector which is either a compound selector (or a simple
|
||||
* selector, which is effectively a compound selector without any compounding) or a selector combinator.
|
||||
*
|
||||
* It allows a selector to be represented as an array of objects that implement this interface.
|
||||
* This is the formal definition:
|
||||
* selector = compound-selector [combinator, compound-selector]*
|
||||
*
|
||||
* The selector is comprised of an array of alternating types that can't be easily represented in a type-safe manner
|
||||
* without this.
|
||||
*
|
||||
* 'Selector component' is not a known grammar in the spec, but a convenience for the implementation.
|
||||
*
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Selectors/Selector_structure
|
||||
* @see https://www.w3.org/TR/selectors-4/#structure
|
||||
*/
|
||||
interface Component extends Renderable
|
||||
{
|
||||
/**
|
||||
* @return non-empty-string
|
||||
*/
|
||||
public function getValue(): string;
|
||||
|
||||
/**
|
||||
* @param non-empty-string $value
|
||||
*/
|
||||
public function setValue(string $value): void;
|
||||
|
||||
/**
|
||||
* @return int<0, max>
|
||||
*/
|
||||
public function getSpecificity(): int;
|
||||
}
|
||||
@@ -0,0 +1,284 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sabberworm\CSS\Property\Selector;
|
||||
|
||||
use Sabberworm\CSS\Comment\Comment;
|
||||
use Sabberworm\CSS\OutputFormat;
|
||||
use Sabberworm\CSS\Parsing\ParserState;
|
||||
use Sabberworm\CSS\Parsing\UnexpectedTokenException;
|
||||
use Sabberworm\CSS\ShortClassNameProvider;
|
||||
|
||||
use function Safe\preg_match;
|
||||
|
||||
/**
|
||||
* Class representing a CSS compound selector.
|
||||
* Selectors have to be split at combinators (space, `>`, `+`, `~`) before being passed to this class.
|
||||
*/
|
||||
class CompoundSelector implements Component
|
||||
{
|
||||
use ShortClassNameProvider;
|
||||
|
||||
private const PARSER_STOP_CHARACTERS = [
|
||||
'{',
|
||||
'}',
|
||||
'\'',
|
||||
'"',
|
||||
'(',
|
||||
')',
|
||||
'[',
|
||||
']',
|
||||
',',
|
||||
' ',
|
||||
"\t",
|
||||
"\n",
|
||||
"\r",
|
||||
'>',
|
||||
'+',
|
||||
'~',
|
||||
ParserState::EOF,
|
||||
'', // `ParserState::peek()` returns empty string rather than `ParserState::EOF` when end of string is reached
|
||||
];
|
||||
|
||||
private const SELECTOR_VALIDATION_RX = '/
|
||||
^
|
||||
# not starting with whitespace
|
||||
(?!\\s)
|
||||
(?:
|
||||
(?:
|
||||
# any sequence of valid unescaped characters, except quotes
|
||||
[a-zA-Z0-9\\x{00A0}-\\x{FFFF}_^$|*=~\\[\\]()\\-\\s\\.:#+>,]++
|
||||
|
|
||||
# one or more escaped characters
|
||||
(?:\\\\.)++
|
||||
|
|
||||
# quoted text, like in `[id="example"]`
|
||||
(?:
|
||||
# opening quote
|
||||
([\'"])
|
||||
(?:
|
||||
# sequence of characters except closing quote or backslash
|
||||
(?:(?!\\g{-1}|\\\\).)++
|
||||
|
|
||||
# one or more escaped characters
|
||||
(?:\\\\.)++
|
||||
)*+ # zero or more times
|
||||
# closing quote or end (unmatched quote is currently allowed)
|
||||
(?:\\g{-1}|$)
|
||||
)
|
||||
)++ # one or more times
|
||||
|
|
||||
# keyframe animation progress percentage (e.g. 50%)
|
||||
(?:\\d++%)
|
||||
)
|
||||
# not ending with whitespace
|
||||
(?<!\\s)
|
||||
$
|
||||
/ux';
|
||||
|
||||
/**
|
||||
* @var non-empty-string
|
||||
*/
|
||||
private $value;
|
||||
|
||||
/**
|
||||
* @param non-empty-string $value
|
||||
*/
|
||||
public function __construct(string $value)
|
||||
{
|
||||
$this->setValue($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<Comment> $comments
|
||||
*
|
||||
* @throws UnexpectedTokenException
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public static function parse(ParserState $parserState, array &$comments = []): self
|
||||
{
|
||||
$selectorParts = [];
|
||||
$stringWrapperCharacter = null;
|
||||
$functionNestingLevel = 0;
|
||||
$isWithinAttribute = false;
|
||||
|
||||
while (true) {
|
||||
$selectorParts[] = $parserState->consumeUntil(self::PARSER_STOP_CHARACTERS, false, false, $comments);
|
||||
$nextCharacter = $parserState->peek();
|
||||
switch ($nextCharacter) {
|
||||
case '':
|
||||
// EOF
|
||||
break 2;
|
||||
case '\'':
|
||||
// The fallthrough is intentional.
|
||||
case '"':
|
||||
$lastPart = \end($selectorParts);
|
||||
$backslashCount = \strspn(\strrev($lastPart), '\\');
|
||||
$quoteIsEscaped = ($backslashCount % 2 === 1);
|
||||
if (!$quoteIsEscaped) {
|
||||
if (!\is_string($stringWrapperCharacter)) {
|
||||
$stringWrapperCharacter = $nextCharacter;
|
||||
} elseif ($stringWrapperCharacter === $nextCharacter) {
|
||||
$stringWrapperCharacter = null;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case '(':
|
||||
if (!\is_string($stringWrapperCharacter)) {
|
||||
++$functionNestingLevel;
|
||||
}
|
||||
break;
|
||||
case ')':
|
||||
if (!\is_string($stringWrapperCharacter)) {
|
||||
if ($functionNestingLevel <= 0) {
|
||||
throw new UnexpectedTokenException(
|
||||
'anything but',
|
||||
')',
|
||||
'literal',
|
||||
$parserState->currentLine()
|
||||
);
|
||||
}
|
||||
--$functionNestingLevel;
|
||||
}
|
||||
break;
|
||||
case '[':
|
||||
if (!\is_string($stringWrapperCharacter)) {
|
||||
if ($isWithinAttribute) {
|
||||
throw new UnexpectedTokenException(
|
||||
'anything but',
|
||||
'[',
|
||||
'literal',
|
||||
$parserState->currentLine()
|
||||
);
|
||||
}
|
||||
$isWithinAttribute = true;
|
||||
}
|
||||
break;
|
||||
case ']':
|
||||
if (!\is_string($stringWrapperCharacter)) {
|
||||
if (!$isWithinAttribute) {
|
||||
throw new UnexpectedTokenException(
|
||||
'anything but',
|
||||
']',
|
||||
'literal',
|
||||
$parserState->currentLine()
|
||||
);
|
||||
}
|
||||
$isWithinAttribute = false;
|
||||
}
|
||||
break;
|
||||
case '{':
|
||||
// The fallthrough is intentional.
|
||||
case '}':
|
||||
if (!\is_string($stringWrapperCharacter)) {
|
||||
break 2;
|
||||
}
|
||||
break;
|
||||
case ',':
|
||||
// The fallthrough is intentional.
|
||||
case ' ':
|
||||
// The fallthrough is intentional.
|
||||
case "\t":
|
||||
// The fallthrough is intentional.
|
||||
case "\n":
|
||||
// The fallthrough is intentional.
|
||||
case "\r":
|
||||
// The fallthrough is intentional.
|
||||
case '>':
|
||||
// The fallthrough is intentional.
|
||||
case '+':
|
||||
// The fallthrough is intentional.
|
||||
case '~':
|
||||
if (!\is_string($stringWrapperCharacter) && $functionNestingLevel === 0 && !$isWithinAttribute) {
|
||||
break 2;
|
||||
}
|
||||
break;
|
||||
}
|
||||
$selectorParts[] = $parserState->consume(1);
|
||||
}
|
||||
|
||||
if ($functionNestingLevel !== 0) {
|
||||
throw new UnexpectedTokenException(')', $nextCharacter, 'literal', $parserState->currentLine());
|
||||
}
|
||||
if (\is_string($stringWrapperCharacter)) {
|
||||
throw new UnexpectedTokenException(
|
||||
$stringWrapperCharacter,
|
||||
$nextCharacter,
|
||||
'literal',
|
||||
$parserState->currentLine()
|
||||
);
|
||||
}
|
||||
|
||||
$value = \implode('', $selectorParts);
|
||||
if ($value === '') {
|
||||
throw new UnexpectedTokenException('selector', $nextCharacter, 'literal', $parserState->currentLine());
|
||||
}
|
||||
if (!self::isValid($value)) {
|
||||
throw new UnexpectedTokenException(
|
||||
'Selector component is not valid:',
|
||||
'`' . $value . '`',
|
||||
'custom',
|
||||
$parserState->currentLine()
|
||||
);
|
||||
}
|
||||
|
||||
return new self($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return non-empty-string
|
||||
*/
|
||||
public function getValue(): string
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param non-empty-string $value
|
||||
*
|
||||
* @throws \UnexpectedValueException if `$value` contains invalid characters or has surrounding whitespce
|
||||
*/
|
||||
public function setValue(string $value): void
|
||||
{
|
||||
if (!self::isValid($value)) {
|
||||
throw new \UnexpectedValueException('`' . $value . '` is not a valid compound selector.');
|
||||
}
|
||||
|
||||
$this->value = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int<0, max>
|
||||
*/
|
||||
public function getSpecificity(): int
|
||||
{
|
||||
return SpecificityCalculator::calculate($this->value);
|
||||
}
|
||||
|
||||
public function render(OutputFormat $outputFormat): string
|
||||
{
|
||||
return $this->getValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, bool|int|float|string|array<mixed>|null>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public function getArrayRepresentation(): array
|
||||
{
|
||||
return [
|
||||
'class' => $this->getShortClassName(),
|
||||
'value' => $this->value,
|
||||
];
|
||||
}
|
||||
|
||||
private static function isValid(string $value): bool
|
||||
{
|
||||
$numberOfMatches = preg_match(self::SELECTOR_VALIDATION_RX, $value);
|
||||
|
||||
return $numberOfMatches === 1;
|
||||
}
|
||||
}
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sabberworm\CSS\Property\Selector;
|
||||
|
||||
use function Safe\preg_match_all;
|
||||
|
||||
/**
|
||||
* Utility class to calculate the specificity of a CSS selector.
|
||||
*
|
||||
* The results are cached to avoid recalculating the specificity of the same selector multiple times.
|
||||
*/
|
||||
final class SpecificityCalculator
|
||||
{
|
||||
/**
|
||||
* regexp for specificity calculations
|
||||
*/
|
||||
private 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
|
||||
*/
|
||||
private const ELEMENTS_AND_PSEUDO_ELEMENTS_RX = '/
|
||||
((^|[\\s\\+\\>\\~]+)[\\w]+ # elements
|
||||
|
|
||||
\\:{1,2}( # pseudo-elements
|
||||
after|before|first-letter|first-line|selection
|
||||
))
|
||||
/ix';
|
||||
|
||||
/**
|
||||
* @var array<string, int<0, max>>
|
||||
*/
|
||||
private static $cache = [];
|
||||
|
||||
/**
|
||||
* Calculates the specificity of the given CSS selector.
|
||||
*
|
||||
* @return int<0, max>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public static function calculate(string $selector): int
|
||||
{
|
||||
if (!isset(self::$cache[$selector])) {
|
||||
$a = 0;
|
||||
/// @todo should exclude \# as well as "#"
|
||||
$matches = null;
|
||||
$b = \substr_count($selector, '#');
|
||||
$c = preg_match_all(self::NON_ID_ATTRIBUTES_AND_PSEUDO_CLASSES_RX, $selector, $matches);
|
||||
$d = preg_match_all(self::ELEMENTS_AND_PSEUDO_ELEMENTS_RX, $selector, $matches);
|
||||
self::$cache[$selector] = ($a * 1000) + ($b * 100) + ($c * 10) + $d;
|
||||
}
|
||||
|
||||
return self::$cache[$selector];
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the cache in order to lower memory usage.
|
||||
*/
|
||||
public static function clearCache(): void
|
||||
{
|
||||
self::$cache = [];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user