Update besar apk finance
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sabberworm\CSS\CSSList;
|
||||
|
||||
use Sabberworm\CSS\OutputFormat;
|
||||
@@ -11,77 +13,59 @@ use Sabberworm\CSS\Property\AtRule;
|
||||
class AtRuleBlockList extends CSSBlockList implements AtRule
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
* @var non-empty-string
|
||||
*/
|
||||
private $sType;
|
||||
private $type;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $sArgs;
|
||||
private $arguments;
|
||||
|
||||
/**
|
||||
* @param string $sType
|
||||
* @param string $sArgs
|
||||
* @param int $iLineNo
|
||||
* @param non-empty-string $type
|
||||
* @param int<1, max>|null $lineNumber
|
||||
*/
|
||||
public function __construct($sType, $sArgs = '', $iLineNo = 0)
|
||||
public function __construct(string $type, string $arguments = '', ?int $lineNumber = null)
|
||||
{
|
||||
parent::__construct($iLineNo);
|
||||
$this->sType = $sType;
|
||||
$this->sArgs = $sArgs;
|
||||
parent::__construct($lineNumber);
|
||||
$this->type = $type;
|
||||
$this->arguments = $arguments;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
* @return non-empty-string
|
||||
*/
|
||||
public function atRuleName()
|
||||
public function atRuleName(): string
|
||||
{
|
||||
return $this->sType;
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
public function atRuleArgs(): string
|
||||
{
|
||||
return $this->arguments;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
* @return non-empty-string
|
||||
*/
|
||||
public function atRuleArgs()
|
||||
public function render(OutputFormat $outputFormat): string
|
||||
{
|
||||
return $this->sArgs;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*
|
||||
* @deprecated in V8.8.0, will be removed in V9.0.0. Use `render` instead.
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
return $this->render(new OutputFormat());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param OutputFormat|null $oOutputFormat
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function render($oOutputFormat)
|
||||
{
|
||||
$sResult = $oOutputFormat->comments($this);
|
||||
$sResult .= $oOutputFormat->sBeforeAtRuleBlock;
|
||||
$sArgs = $this->sArgs;
|
||||
if ($sArgs) {
|
||||
$sArgs = ' ' . $sArgs;
|
||||
$formatter = $outputFormat->getFormatter();
|
||||
$result = $formatter->comments($this);
|
||||
$result .= $outputFormat->getContentBeforeAtRuleBlock();
|
||||
$arguments = $this->arguments;
|
||||
if ($arguments !== '') {
|
||||
$arguments = ' ' . $arguments;
|
||||
}
|
||||
$sResult .= "@{$this->sType}$sArgs{$oOutputFormat->spaceBeforeOpeningBrace()}{";
|
||||
$sResult .= $this->renderListContents($oOutputFormat);
|
||||
$sResult .= '}';
|
||||
$sResult .= $oOutputFormat->sAfterAtRuleBlock;
|
||||
return $sResult;
|
||||
$result .= "@{$this->type}$arguments{$formatter->spaceBeforeOpeningBrace()}{";
|
||||
$result .= $this->renderListContents($outputFormat);
|
||||
$result .= '}';
|
||||
$result .= $outputFormat->getContentAfterAtRuleBlock();
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isRootList()
|
||||
public function isRootList(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
+127
-134
@@ -1,11 +1,14 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sabberworm\CSS\CSSList;
|
||||
|
||||
use Sabberworm\CSS\CSSElement;
|
||||
use Sabberworm\CSS\Property\Declaration;
|
||||
use Sabberworm\CSS\Property\Selector;
|
||||
use Sabberworm\CSS\Rule\Rule;
|
||||
use Sabberworm\CSS\RuleSet\DeclarationBlock;
|
||||
use Sabberworm\CSS\RuleSet\DeclarationList;
|
||||
use Sabberworm\CSS\RuleSet\RuleSet;
|
||||
use Sabberworm\CSS\Value\CSSFunction;
|
||||
use Sabberworm\CSS\Value\Value;
|
||||
@@ -20,171 +23,161 @@ use Sabberworm\CSS\Value\ValueList;
|
||||
abstract class CSSBlockList extends CSSList
|
||||
{
|
||||
/**
|
||||
* @param int $iLineNo
|
||||
* Gets all `DeclarationBlock` objects recursively, no matter how deeply nested the selectors are.
|
||||
*
|
||||
* @return list<DeclarationBlock>
|
||||
*/
|
||||
public function __construct($iLineNo = 0)
|
||||
public function getAllDeclarationBlocks(): array
|
||||
{
|
||||
parent::__construct($iLineNo);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, DeclarationBlock> $aResult
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function allDeclarationBlocks(array &$aResult)
|
||||
{
|
||||
foreach ($this->aContents as $mContent) {
|
||||
if ($mContent instanceof DeclarationBlock) {
|
||||
$aResult[] = $mContent;
|
||||
} elseif ($mContent instanceof CSSBlockList) {
|
||||
$mContent->allDeclarationBlocks($aResult);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, RuleSet> $aResult
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function allRuleSets(array &$aResult)
|
||||
{
|
||||
foreach ($this->aContents as $mContent) {
|
||||
if ($mContent instanceof RuleSet) {
|
||||
$aResult[] = $mContent;
|
||||
} elseif ($mContent instanceof CSSBlockList) {
|
||||
$mContent->allRuleSets($aResult);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all `Value` objects found recursively in `Rule`s in the tree.
|
||||
*
|
||||
* @param CSSElement|string|null $element
|
||||
* This is the `CSSList` or `RuleSet` to start the search from (defaults to the whole document).
|
||||
* If a string is given, it is used as a rule name filter.
|
||||
* Passing a string for this parameter is deprecated in version 8.9.0, and will not work from v9.0;
|
||||
* use the following parameter to pass a rule name filter instead.
|
||||
* @param string|bool|null $ruleSearchPatternOrSearchInFunctionArguments
|
||||
* This allows filtering rules by property name
|
||||
* (e.g. if "color" is passed, only `Value`s from `color` properties will be returned,
|
||||
* or if "font-" is provided, `Value`s from all font rules, like `font-size`, and including `font` itself,
|
||||
* will be returned).
|
||||
* If a Boolean is provided, it is treated as the `$searchInFunctionArguments` argument.
|
||||
* Passing a Boolean for this parameter is deprecated in version 8.9.0, and will not work from v9.0;
|
||||
* use the `$searchInFunctionArguments` parameter instead.
|
||||
* @param bool $searchInFunctionArguments whether to also return Value objects used as Function arguments.
|
||||
*
|
||||
* @return array<int, Value>
|
||||
*
|
||||
* @see RuleSet->getRules()
|
||||
*/
|
||||
public function getAllValues(
|
||||
$element = null,
|
||||
$ruleSearchPatternOrSearchInFunctionArguments = null,
|
||||
$searchInFunctionArguments = false
|
||||
) {
|
||||
if (\is_bool($ruleSearchPatternOrSearchInFunctionArguments)) {
|
||||
$searchInFunctionArguments = $ruleSearchPatternOrSearchInFunctionArguments;
|
||||
$searchString = null;
|
||||
} else {
|
||||
$searchString = $ruleSearchPatternOrSearchInFunctionArguments;
|
||||
}
|
||||
|
||||
if ($element === null) {
|
||||
$element = $this;
|
||||
} elseif (\is_string($element)) {
|
||||
$searchString = $element;
|
||||
$element = $this;
|
||||
}
|
||||
|
||||
$result = [];
|
||||
$this->allValues($element, $result, $searchString, $searchInFunctionArguments);
|
||||
|
||||
foreach ($this->contents as $item) {
|
||||
if ($item instanceof DeclarationBlock) {
|
||||
$result[] = $item;
|
||||
} elseif ($item instanceof CSSBlockList) {
|
||||
$result = \array_merge($result, $item->getAllDeclarationBlocks());
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param CSSElement|string $oElement
|
||||
* @param array<int, Value> $aResult
|
||||
* @param string|null $sSearchString
|
||||
* @param bool $bSearchInFunctionArguments
|
||||
* Returns all `RuleSet` objects recursively found in the tree, no matter how deeply nested the rule sets are.
|
||||
*
|
||||
* @return void
|
||||
* @return list<RuleSet>
|
||||
*/
|
||||
protected function allValues($oElement, array &$aResult, $sSearchString = null, $bSearchInFunctionArguments = false)
|
||||
public function getAllRuleSets(): array
|
||||
{
|
||||
if ($oElement instanceof CSSBlockList) {
|
||||
foreach ($oElement->getContents() as $oContent) {
|
||||
$this->allValues($oContent, $aResult, $sSearchString, $bSearchInFunctionArguments);
|
||||
$result = [];
|
||||
|
||||
foreach ($this->contents as $item) {
|
||||
if ($item instanceof RuleSet) {
|
||||
$result[] = $item;
|
||||
} elseif ($item instanceof CSSBlockList) {
|
||||
$result = \array_merge($result, $item->getAllRuleSets());
|
||||
} elseif ($item instanceof DeclarationBlock) {
|
||||
$result[] = $item->getRuleSet();
|
||||
}
|
||||
} 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;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, Selector> $aResult
|
||||
* @param string|null $sSpecificitySearch
|
||||
* Returns all `Value` objects found recursively in `Declaration`s in the tree.
|
||||
*
|
||||
* @return void
|
||||
* @param CSSElement|null $element
|
||||
* This is the `CSSList` or `RuleSet` to start the search from (defaults to the whole document).
|
||||
* @param string|null $ruleSearchPattern
|
||||
* This allows filtering rules by property name
|
||||
* (e.g. if "color" is passed, only `Value`s from `color` properties will be returned,
|
||||
* or if "font-" is provided, `Value`s from all font rules, like `font-size`, and including `font` itself,
|
||||
* will be returned).
|
||||
* @param bool $searchInFunctionArguments whether to also return `Value` objects used as `CSSFunction` arguments.
|
||||
*
|
||||
* @return list<Value>
|
||||
*
|
||||
* @see RuleSet->getRules()
|
||||
*/
|
||||
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];
|
||||
public function getAllValues(
|
||||
?CSSElement $element = null,
|
||||
?string $ruleSearchPattern = null,
|
||||
bool $searchInFunctionArguments = false
|
||||
): array {
|
||||
$element = $element ?? $this;
|
||||
|
||||
$result = [];
|
||||
if ($element instanceof CSSBlockList) {
|
||||
foreach ($element->getContents() as $contentItem) {
|
||||
// Statement at-rules are skipped since they do not contain values.
|
||||
if ($contentItem instanceof CSSElement) {
|
||||
$result = \array_merge(
|
||||
$result,
|
||||
$this->getAllValues($contentItem, $ruleSearchPattern, $searchInFunctionArguments)
|
||||
);
|
||||
}
|
||||
}
|
||||
} elseif ($element instanceof DeclarationList) {
|
||||
foreach ($element->getRules($ruleSearchPattern) as $rule) {
|
||||
$result = \array_merge(
|
||||
$result,
|
||||
$this->getAllValues($rule, $ruleSearchPattern, $searchInFunctionArguments)
|
||||
);
|
||||
}
|
||||
} elseif ($element instanceof Declaration) {
|
||||
$value = $element->getValue();
|
||||
// `string` values are discarded.
|
||||
if ($value instanceof CSSElement) {
|
||||
$result = \array_merge(
|
||||
$result,
|
||||
$this->getAllValues($value, $ruleSearchPattern, $searchInFunctionArguments)
|
||||
);
|
||||
}
|
||||
} elseif ($element instanceof ValueList) {
|
||||
if ($searchInFunctionArguments || !($element instanceof CSSFunction)) {
|
||||
foreach ($element->getListComponents() as $component) {
|
||||
// `string` components are discarded.
|
||||
if ($component instanceof CSSElement) {
|
||||
$result = \array_merge(
|
||||
$result,
|
||||
$this->getAllValues($component, $ruleSearchPattern, $searchInFunctionArguments)
|
||||
);
|
||||
}
|
||||
$iTargetSpecificity = (int)$iTargetSpecificity;
|
||||
$iSelectorSpecificity = $oSelector->getSpecificity();
|
||||
$bMatches = false;
|
||||
switch ($sComparator) {
|
||||
}
|
||||
}
|
||||
} elseif ($element instanceof Value) {
|
||||
$result[] = $element;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<Selector>
|
||||
*/
|
||||
protected function getAllSelectors(?string $specificitySearch = null): array
|
||||
{
|
||||
$result = [];
|
||||
|
||||
foreach ($this->getAllDeclarationBlocks() as $declarationBlock) {
|
||||
foreach ($declarationBlock->getSelectors() as $selector) {
|
||||
if ($specificitySearch === null) {
|
||||
$result[] = $selector;
|
||||
} else {
|
||||
$comparator = '===';
|
||||
$expressionParts = \explode(' ', $specificitySearch);
|
||||
$targetSpecificity = $expressionParts[0];
|
||||
if (\count($expressionParts) > 1) {
|
||||
$comparator = $expressionParts[0];
|
||||
$targetSpecificity = $expressionParts[1];
|
||||
}
|
||||
$targetSpecificity = (int) $targetSpecificity;
|
||||
$selectorSpecificity = $selector->getSpecificity();
|
||||
switch ($comparator) {
|
||||
case '<=':
|
||||
$bMatches = $iSelectorSpecificity <= $iTargetSpecificity;
|
||||
$comparatorMatched = $selectorSpecificity <= $targetSpecificity;
|
||||
break;
|
||||
case '<':
|
||||
$bMatches = $iSelectorSpecificity < $iTargetSpecificity;
|
||||
$comparatorMatched = $selectorSpecificity < $targetSpecificity;
|
||||
break;
|
||||
case '>=':
|
||||
$bMatches = $iSelectorSpecificity >= $iTargetSpecificity;
|
||||
$comparatorMatched = $selectorSpecificity >= $targetSpecificity;
|
||||
break;
|
||||
case '>':
|
||||
$bMatches = $iSelectorSpecificity > $iTargetSpecificity;
|
||||
$comparatorMatched = $selectorSpecificity > $targetSpecificity;
|
||||
break;
|
||||
default:
|
||||
$bMatches = $iSelectorSpecificity === $iTargetSpecificity;
|
||||
$comparatorMatched = $selectorSpecificity === $targetSpecificity;
|
||||
break;
|
||||
}
|
||||
if ($bMatches) {
|
||||
$aResult[] = $oSelector;
|
||||
if ($comparatorMatched) {
|
||||
$result[] = $selector;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
+234
-252
@@ -1,9 +1,10 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sabberworm\CSS\CSSList;
|
||||
|
||||
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;
|
||||
@@ -17,293 +18,276 @@ 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;
|
||||
|
||||
use function Safe\preg_match;
|
||||
|
||||
/**
|
||||
* This is the most generic container available. It can contain `DeclarationBlock`s (rule sets with a selector),
|
||||
* `RuleSet`s as well as other `CSSList` objects.
|
||||
*
|
||||
* It can also contain `Import` and `Charset` objects stemming from at-rules.
|
||||
*
|
||||
* Note that `CSSListItem` extends both `Commentable` and `Renderable`,
|
||||
* so those interfaces must also be implemented by concrete subclasses.
|
||||
*/
|
||||
abstract class CSSList implements Commentable, CSSElement, Positionable
|
||||
abstract class CSSList implements CSSElement, CSSListItem, Positionable
|
||||
{
|
||||
use CommentContainer;
|
||||
use Position;
|
||||
|
||||
/**
|
||||
* @var array<array-key, Comment>
|
||||
* @var array<int<0, max>, CSSListItem>
|
||||
*
|
||||
* @internal since 8.8.0
|
||||
*/
|
||||
protected $aComments;
|
||||
protected $contents = [];
|
||||
|
||||
/**
|
||||
* @var array<int, RuleSet|CSSList|Import|Charset>
|
||||
*
|
||||
* @internal since 8.8.0
|
||||
* @param int<1, max>|null $lineNumber
|
||||
*/
|
||||
protected $aContents;
|
||||
|
||||
/**
|
||||
* @param int $iLineNo
|
||||
*/
|
||||
public function __construct($iLineNo = 0)
|
||||
public function __construct(?int $lineNumber = null)
|
||||
{
|
||||
$this->aComments = [];
|
||||
$this->aContents = [];
|
||||
$this->setPosition($iLineNo);
|
||||
$this->setPosition($lineNumber);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*
|
||||
* @throws UnexpectedTokenException
|
||||
* @throws SourceException
|
||||
*
|
||||
* @internal since V8.8.0
|
||||
*/
|
||||
public static function parseList(ParserState $oParserState, CSSList $oList)
|
||||
public static function parseList(ParserState $parserState, CSSList $list): void
|
||||
{
|
||||
$bIsRoot = $oList instanceof Document;
|
||||
if (is_string($oParserState)) {
|
||||
$oParserState = new ParserState($oParserState, Settings::create());
|
||||
}
|
||||
$bLenientParsing = $oParserState->getSettings()->bLenientParsing;
|
||||
$aComments = [];
|
||||
while (!$oParserState->isEnd()) {
|
||||
$aComments = array_merge($aComments, $oParserState->consumeWhiteSpace());
|
||||
$oListItem = null;
|
||||
if ($bLenientParsing) {
|
||||
$isRoot = $list instanceof Document;
|
||||
$usesLenientParsing = $parserState->getSettings()->usesLenientParsing();
|
||||
$comments = [];
|
||||
$parserState->consumeWhiteSpace($comments);
|
||||
while (!$parserState->isEnd()) {
|
||||
$listItem = null;
|
||||
if ($usesLenientParsing) {
|
||||
try {
|
||||
$oListItem = self::parseListItem($oParserState, $oList);
|
||||
$positionBeforeParse = $parserState->currentColumn();
|
||||
$listItem = self::parseListItem($parserState, $list);
|
||||
} catch (UnexpectedTokenException $e) {
|
||||
$oListItem = false;
|
||||
$listItem = false;
|
||||
// If the failed parsing did not consume anything that was to come ...
|
||||
if ($parserState->currentColumn() === $positionBeforeParse) {
|
||||
// ... the unexpected token needs to be skipped, otherwise there'll be an infinite loop.
|
||||
$parserState->consume(1);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$oListItem = self::parseListItem($oParserState, $oList);
|
||||
$listItem = self::parseListItem($parserState, $list);
|
||||
}
|
||||
if ($oListItem === null) {
|
||||
if ($listItem === null) {
|
||||
// List parsing finished
|
||||
return;
|
||||
}
|
||||
if ($oListItem) {
|
||||
$oListItem->addComments($aComments);
|
||||
$oList->append($oListItem);
|
||||
if ($listItem) {
|
||||
$listItem->addComments($comments);
|
||||
$list->append($listItem);
|
||||
}
|
||||
$aComments = $oParserState->consumeWhiteSpace();
|
||||
$comments = [];
|
||||
$parserState->consumeWhiteSpace($comments);
|
||||
}
|
||||
$oList->addComments($aComments);
|
||||
if (!$bIsRoot && !$bLenientParsing) {
|
||||
throw new SourceException("Unexpected end of document", $oParserState->currentLine());
|
||||
$list->addComments($comments);
|
||||
if (!$isRoot && !$usesLenientParsing) {
|
||||
throw new SourceException('Unexpected end of document', $parserState->currentLine());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return AtRuleBlockList|KeyFrame|Charset|CSSNamespace|Import|AtRuleSet|DeclarationBlock|null|false
|
||||
* @return CSSListItem|false|null
|
||||
* If `null` is returned, it means the end of the list has been reached.
|
||||
* If `false` is returned, it means an invalid item has been encountered,
|
||||
* but parsing of the next item should proceed.
|
||||
*
|
||||
* @throws SourceException
|
||||
* @throws UnexpectedEOFException
|
||||
* @throws UnexpectedTokenException
|
||||
*/
|
||||
private static function parseListItem(ParserState $oParserState, CSSList $oList)
|
||||
private static function parseListItem(ParserState $parserState, CSSList $list)
|
||||
{
|
||||
$bIsRoot = $oList instanceof Document;
|
||||
if ($oParserState->comes('@')) {
|
||||
$oAtRule = self::parseAtRule($oParserState);
|
||||
if ($oAtRule instanceof Charset) {
|
||||
if (!$bIsRoot) {
|
||||
$isRoot = $list instanceof Document;
|
||||
if ($parserState->comes('@')) {
|
||||
$atRule = self::parseAtRule($parserState);
|
||||
if ($atRule instanceof Charset) {
|
||||
if (!$isRoot) {
|
||||
throw new UnexpectedTokenException(
|
||||
'@charset may only occur in root document',
|
||||
'',
|
||||
'custom',
|
||||
$oParserState->currentLine()
|
||||
$parserState->currentLine()
|
||||
);
|
||||
}
|
||||
if (count($oList->getContents()) > 0) {
|
||||
if (\count($list->getContents()) > 0) {
|
||||
throw new UnexpectedTokenException(
|
||||
'@charset must be the first parseable token in a document',
|
||||
'',
|
||||
'custom',
|
||||
$oParserState->currentLine()
|
||||
$parserState->currentLine()
|
||||
);
|
||||
}
|
||||
$oParserState->setCharset($oAtRule->getCharset());
|
||||
$parserState->setCharset($atRule->getCharset());
|
||||
}
|
||||
return $oAtRule;
|
||||
} elseif ($oParserState->comes('}')) {
|
||||
if ($bIsRoot) {
|
||||
if ($oParserState->getSettings()->bLenientParsing) {
|
||||
return DeclarationBlock::parse($oParserState);
|
||||
return $atRule;
|
||||
} elseif ($parserState->comes('}')) {
|
||||
if ($isRoot) {
|
||||
if ($parserState->getSettings()->usesLenientParsing()) {
|
||||
$parserState->consume(1);
|
||||
return self::parseListItem($parserState, $list);
|
||||
} else {
|
||||
throw new SourceException("Unopened {", $oParserState->currentLine());
|
||||
throw new SourceException('Unopened {', $parserState->currentLine());
|
||||
}
|
||||
} else {
|
||||
// End of list
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
return DeclarationBlock::parse($oParserState, $oList);
|
||||
return DeclarationBlock::parse($parserState, $list) ?? false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ParserState $oParserState
|
||||
*
|
||||
* @return AtRuleBlockList|KeyFrame|Charset|CSSNamespace|Import|AtRuleSet|null
|
||||
*
|
||||
* @throws SourceException
|
||||
* @throws UnexpectedTokenException
|
||||
* @throws UnexpectedEOFException
|
||||
*/
|
||||
private static function parseAtRule(ParserState $oParserState)
|
||||
private static function parseAtRule(ParserState $parserState): ?CSSListItem
|
||||
{
|
||||
$oParserState->consume('@');
|
||||
$sIdentifier = $oParserState->parseIdentifier();
|
||||
$iIdentifierLineNum = $oParserState->currentLine();
|
||||
$oParserState->consumeWhiteSpace();
|
||||
if ($sIdentifier === 'import') {
|
||||
$oLocation = URL::parse($oParserState);
|
||||
$oParserState->consumeWhiteSpace();
|
||||
$sMediaQuery = null;
|
||||
if (!$oParserState->comes(';')) {
|
||||
$sMediaQuery = trim($oParserState->consumeUntil([';', ParserState::EOF]));
|
||||
}
|
||||
$oParserState->consumeUntil([';', ParserState::EOF], true, true);
|
||||
return new Import($oLocation, $sMediaQuery ?: null, $iIdentifierLineNum);
|
||||
} elseif ($sIdentifier === 'charset') {
|
||||
$oCharsetString = CSSString::parse($oParserState);
|
||||
$oParserState->consumeWhiteSpace();
|
||||
$oParserState->consumeUntil([';', ParserState::EOF], true, true);
|
||||
return new Charset($oCharsetString, $iIdentifierLineNum);
|
||||
} elseif (self::identifierIs($sIdentifier, 'keyframes')) {
|
||||
$oResult = new KeyFrame($iIdentifierLineNum);
|
||||
$oResult->setVendorKeyFrame($sIdentifier);
|
||||
$oResult->setAnimationName(trim($oParserState->consumeUntil('{', false, true)));
|
||||
CSSList::parseList($oParserState, $oResult);
|
||||
if ($oParserState->comes('}')) {
|
||||
$oParserState->consume('}');
|
||||
}
|
||||
return $oResult;
|
||||
} elseif ($sIdentifier === 'namespace') {
|
||||
$sPrefix = null;
|
||||
$mUrl = Value::parsePrimitiveValue($oParserState);
|
||||
if (!$oParserState->comes(';')) {
|
||||
$sPrefix = $mUrl;
|
||||
$mUrl = Value::parsePrimitiveValue($oParserState);
|
||||
}
|
||||
$oParserState->consumeUntil([';', ParserState::EOF], true, true);
|
||||
if ($sPrefix !== null && !is_string($sPrefix)) {
|
||||
throw new UnexpectedTokenException('Wrong namespace prefix', $sPrefix, 'custom', $iIdentifierLineNum);
|
||||
}
|
||||
if (!($mUrl instanceof CSSString || $mUrl instanceof URL)) {
|
||||
throw new UnexpectedTokenException(
|
||||
'Wrong namespace url of invalid type',
|
||||
$mUrl,
|
||||
'custom',
|
||||
$iIdentifierLineNum
|
||||
);
|
||||
}
|
||||
return new CSSNamespace($mUrl, $sPrefix, $iIdentifierLineNum);
|
||||
} else {
|
||||
// Unknown other at rule (font-face or such)
|
||||
$sArgs = trim($oParserState->consumeUntil('{', false, true));
|
||||
if (substr_count($sArgs, "(") != substr_count($sArgs, ")")) {
|
||||
if ($oParserState->getSettings()->bLenientParsing) {
|
||||
return null;
|
||||
} else {
|
||||
throw new SourceException("Unmatched brace count in media query", $oParserState->currentLine());
|
||||
$parserState->consume('@');
|
||||
$identifier = $parserState->parseIdentifier();
|
||||
$identifierLineNumber = $parserState->currentLine();
|
||||
$parserState->consumeWhiteSpace();
|
||||
if ($identifier === 'import') {
|
||||
$location = URL::parse($parserState);
|
||||
$parserState->consumeWhiteSpace();
|
||||
$mediaQuery = null;
|
||||
if (!$parserState->comes(';')) {
|
||||
$mediaQuery = \trim($parserState->consumeUntil([';', ParserState::EOF]));
|
||||
if ($mediaQuery === '') {
|
||||
$mediaQuery = null;
|
||||
}
|
||||
}
|
||||
$bUseRuleSet = true;
|
||||
foreach (explode('/', AtRule::BLOCK_RULES) as $sBlockRuleName) {
|
||||
if (self::identifierIs($sIdentifier, $sBlockRuleName)) {
|
||||
$bUseRuleSet = false;
|
||||
$parserState->consumeUntil([';', ParserState::EOF], true, true);
|
||||
return new Import($location, $mediaQuery, $identifierLineNumber);
|
||||
} elseif ($identifier === 'charset') {
|
||||
$charsetString = CSSString::parse($parserState);
|
||||
$parserState->consumeWhiteSpace();
|
||||
$parserState->consumeUntil([';', ParserState::EOF], true, true);
|
||||
return new Charset($charsetString, $identifierLineNumber);
|
||||
} elseif (self::identifierIs($identifier, 'keyframes')) {
|
||||
$result = new KeyFrame($identifierLineNumber);
|
||||
$result->setVendorKeyFrame($identifier);
|
||||
$result->setAnimationName(\trim($parserState->consumeUntil('{', false, true)));
|
||||
CSSList::parseList($parserState, $result);
|
||||
if ($parserState->comes('}')) {
|
||||
$parserState->consume('}');
|
||||
}
|
||||
return $result;
|
||||
} elseif ($identifier === 'namespace') {
|
||||
$prefix = null;
|
||||
$url = Value::parsePrimitiveValue($parserState);
|
||||
if (!$parserState->comes(';')) {
|
||||
$prefix = $url;
|
||||
$url = Value::parsePrimitiveValue($parserState);
|
||||
}
|
||||
$parserState->consumeUntil([';', ParserState::EOF], true, true);
|
||||
if ($prefix !== null && !\is_string($prefix)) {
|
||||
throw new UnexpectedTokenException('Wrong namespace prefix', $prefix, 'custom', $identifierLineNumber);
|
||||
}
|
||||
if (!($url instanceof CSSString || $url instanceof URL)) {
|
||||
throw new UnexpectedTokenException(
|
||||
'Wrong namespace url of invalid type',
|
||||
$url,
|
||||
'custom',
|
||||
$identifierLineNumber
|
||||
);
|
||||
}
|
||||
return new CSSNamespace($url, $prefix, $identifierLineNumber);
|
||||
} else {
|
||||
// Unknown other at rule (font-face or such)
|
||||
$arguments = \trim($parserState->consumeUntil('{', false, true));
|
||||
if (\substr_count($arguments, '(') !== \substr_count($arguments, ')')) {
|
||||
if ($parserState->getSettings()->usesLenientParsing()) {
|
||||
return null;
|
||||
} else {
|
||||
throw new SourceException('Unmatched brace count in media query', $parserState->currentLine());
|
||||
}
|
||||
}
|
||||
$useRuleSet = true;
|
||||
foreach (AtRule::BLOCK_RULES as $blockRuleName) {
|
||||
if (self::identifierIs($identifier, $blockRuleName)) {
|
||||
$useRuleSet = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($bUseRuleSet) {
|
||||
$oAtRule = new AtRuleSet($sIdentifier, $sArgs, $iIdentifierLineNum);
|
||||
RuleSet::parseRuleSet($oParserState, $oAtRule);
|
||||
if ($useRuleSet) {
|
||||
$atRule = new AtRuleSet($identifier, $arguments, $identifierLineNumber);
|
||||
RuleSet::parseRuleSet($parserState, $atRule);
|
||||
} else {
|
||||
$oAtRule = new AtRuleBlockList($sIdentifier, $sArgs, $iIdentifierLineNum);
|
||||
CSSList::parseList($oParserState, $oAtRule);
|
||||
if ($oParserState->comes('}')) {
|
||||
$oParserState->consume('}');
|
||||
$atRule = new AtRuleBlockList($identifier, $arguments, $identifierLineNumber);
|
||||
CSSList::parseList($parserState, $atRule);
|
||||
if ($parserState->comes('}')) {
|
||||
$parserState->consume('}');
|
||||
}
|
||||
}
|
||||
return $oAtRule;
|
||||
return $atRule;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
private static function identifierIs(string $identifier, string $match): bool
|
||||
{
|
||||
return (strcasecmp($sIdentifier, $sMatch) === 0)
|
||||
?: preg_match("/^(-\\w+-)?$sMatch$/i", $sIdentifier) === 1;
|
||||
if (\strcasecmp($identifier, $match) === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return preg_match("/^(-\\w+-)?$match$/i", $identifier) === 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepends an item to the list of contents.
|
||||
*
|
||||
* @param RuleSet|CSSList|Import|Charset $oItem
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function prepend($oItem)
|
||||
public function prepend(CSSListItem $item): void
|
||||
{
|
||||
array_unshift($this->aContents, $oItem);
|
||||
\array_unshift($this->contents, $item);
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends an item to the list of contents.
|
||||
*
|
||||
* @param RuleSet|CSSList|Import|Charset $oItem
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function append($oItem)
|
||||
public function append(CSSListItem $item): void
|
||||
{
|
||||
$this->aContents[] = $oItem;
|
||||
$this->contents[] = $item;
|
||||
}
|
||||
|
||||
/**
|
||||
* Splices the list of contents.
|
||||
*
|
||||
* @param int $iOffset
|
||||
* @param int $iLength
|
||||
* @param array<int, RuleSet|CSSList|Import|Charset> $mReplacement
|
||||
*
|
||||
* @return void
|
||||
* @param array<int, CSSListItem> $replacement
|
||||
*/
|
||||
public function splice($iOffset, $iLength = null, $mReplacement = null)
|
||||
public function splice(int $offset, ?int $length = null, ?array $replacement = null): void
|
||||
{
|
||||
array_splice($this->aContents, $iOffset, $iLength, $mReplacement);
|
||||
\array_splice($this->contents, $offset, $length, $replacement);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts an item in the CSS list before its sibling. If the desired sibling cannot be found,
|
||||
* the item is appended at the end.
|
||||
*
|
||||
* @param RuleSet|CSSList|Import|Charset $item
|
||||
* @param RuleSet|CSSList|Import|Charset $sibling
|
||||
*/
|
||||
public function insertBefore($item, $sibling)
|
||||
public function insertBefore(CSSListItem $item, CSSListItem $sibling): void
|
||||
{
|
||||
if (in_array($sibling, $this->aContents, true)) {
|
||||
if (\in_array($sibling, $this->contents, true)) {
|
||||
$this->replace($sibling, [$item, $sibling]);
|
||||
} else {
|
||||
$this->append($item);
|
||||
@@ -313,52 +297,53 @@ abstract class CSSList implements Commentable, CSSElement, Positionable
|
||||
/**
|
||||
* 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)
|
||||
* @param CSSListItem $itemToRemove
|
||||
* May be a `RuleSet` (most likely a `DeclarationBlock`), an `Import`,
|
||||
* a `Charset` or another `CSSList` (most likely a `MediaQuery`)
|
||||
*
|
||||
* @return bool whether the item was removed
|
||||
*/
|
||||
public function remove($oItemToRemove)
|
||||
public function remove(CSSListItem $itemToRemove): bool
|
||||
{
|
||||
$iKey = array_search($oItemToRemove, $this->aContents, true);
|
||||
if ($iKey !== false) {
|
||||
unset($this->aContents[$iKey]);
|
||||
$key = \array_search($itemToRemove, $this->contents, true);
|
||||
if ($key !== false) {
|
||||
unset($this->contents[$key]);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces an item from the CSS list.
|
||||
*
|
||||
* @param RuleSet|Import|Charset|CSSList $oOldItem
|
||||
* @param CSSListItem $oldItem
|
||||
* May be a `RuleSet` (most likely a `DeclarationBlock`), an `Import`, a `Charset`
|
||||
* or another `CSSList` (most likely a `MediaQuery`)
|
||||
*
|
||||
* @return bool
|
||||
* @param CSSListItem|array<CSSListItem> $newItem
|
||||
*/
|
||||
public function replace($oOldItem, $mNewItem)
|
||||
public function replace(CSSListItem $oldItem, $newItem): bool
|
||||
{
|
||||
$iKey = array_search($oOldItem, $this->aContents, true);
|
||||
if ($iKey !== false) {
|
||||
if (is_array($mNewItem)) {
|
||||
array_splice($this->aContents, $iKey, 1, $mNewItem);
|
||||
$key = \array_search($oldItem, $this->contents, true);
|
||||
if ($key !== false) {
|
||||
if (\is_array($newItem)) {
|
||||
\array_splice($this->contents, $key, 1, $newItem);
|
||||
} else {
|
||||
array_splice($this->aContents, $iKey, 1, [$mNewItem]);
|
||||
\array_splice($this->contents, $key, 1, [$newItem]);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, RuleSet|Import|Charset|CSSList> $aContents
|
||||
* @param array<int, CSSListItem> $contents
|
||||
*/
|
||||
public function setContents(array $aContents)
|
||||
public function setContents(array $contents): void
|
||||
{
|
||||
$this->aContents = [];
|
||||
foreach ($aContents as $content) {
|
||||
$this->contents = [];
|
||||
foreach ($contents as $content) {
|
||||
$this->append($content);
|
||||
}
|
||||
}
|
||||
@@ -366,131 +351,128 @@ abstract class CSSList implements Commentable, CSSElement, Positionable
|
||||
/**
|
||||
* 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
|
||||
* @param DeclarationBlock|array<Selector>|string $selectors the selectors to match
|
||||
* @param bool $removeAll whether to stop at the first declaration block found or remove all blocks
|
||||
*/
|
||||
public function removeDeclarationBlockBySelector($mSelector, $bRemoveAll = false)
|
||||
public function removeDeclarationBlockBySelector($selectors, bool $removeAll = false): void
|
||||
{
|
||||
if ($mSelector instanceof DeclarationBlock) {
|
||||
$mSelector = $mSelector->getSelectors();
|
||||
if ($selectors instanceof DeclarationBlock) {
|
||||
$selectors = $selectors->getSelectors();
|
||||
}
|
||||
if (!is_array($mSelector)) {
|
||||
$mSelector = explode(',', $mSelector);
|
||||
if (!\is_array($selectors)) {
|
||||
$selectors = \explode(',', $selectors);
|
||||
}
|
||||
foreach ($mSelector as $iKey => &$mSel) {
|
||||
if (!($mSel instanceof Selector)) {
|
||||
if (!Selector::isValid($mSel)) {
|
||||
foreach ($selectors as &$selector) {
|
||||
if (!($selector instanceof Selector)) {
|
||||
if (!Selector::isValid($selector)) {
|
||||
throw new UnexpectedTokenException(
|
||||
"Selector did not match '" . Selector::SELECTOR_VALIDATION_RX . "'.",
|
||||
$mSel,
|
||||
"custom"
|
||||
$selector,
|
||||
'custom'
|
||||
);
|
||||
}
|
||||
$mSel = new Selector($mSel);
|
||||
$selector = new Selector($selector);
|
||||
}
|
||||
}
|
||||
foreach ($this->aContents as $iKey => $mItem) {
|
||||
if (!($mItem instanceof DeclarationBlock)) {
|
||||
foreach ($this->contents as $key => $item) {
|
||||
if (!($item instanceof DeclarationBlock)) {
|
||||
continue;
|
||||
}
|
||||
if ($mItem->getSelectors() == $mSelector) {
|
||||
unset($this->aContents[$iKey]);
|
||||
if (!$bRemoveAll) {
|
||||
if (self::selectorsMatch($item->getSelectors(), $selectors)) {
|
||||
unset($this->contents[$key]);
|
||||
if (!$removeAll) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*
|
||||
* @deprecated in V8.8.0, will be removed in V9.0.0. Use `render` instead.
|
||||
*/
|
||||
public function __toString()
|
||||
protected function renderListContents(OutputFormat $outputFormat): string
|
||||
{
|
||||
return $this->render(new OutputFormat());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
protected function renderListContents(OutputFormat $oOutputFormat)
|
||||
{
|
||||
$sResult = '';
|
||||
$bIsFirst = true;
|
||||
$oNextLevel = $oOutputFormat;
|
||||
$result = '';
|
||||
$isFirst = true;
|
||||
$nextLevelFormat = $outputFormat;
|
||||
if (!$this->isRootList()) {
|
||||
$oNextLevel = $oOutputFormat->nextLevel();
|
||||
$nextLevelFormat = $outputFormat->nextLevel();
|
||||
}
|
||||
foreach ($this->aContents as $oContent) {
|
||||
$sRendered = $oOutputFormat->safely(function () use ($oNextLevel, $oContent) {
|
||||
return $oContent->render($oNextLevel);
|
||||
$nextLevelFormatter = $nextLevelFormat->getFormatter();
|
||||
$formatter = $outputFormat->getFormatter();
|
||||
foreach ($this->contents as $listItem) {
|
||||
$renderedCss = $formatter->safely(static function () use ($nextLevelFormat, $listItem): string {
|
||||
return $listItem->render($nextLevelFormat);
|
||||
});
|
||||
if ($sRendered === null) {
|
||||
if ($renderedCss === null) {
|
||||
continue;
|
||||
}
|
||||
if ($bIsFirst) {
|
||||
$bIsFirst = false;
|
||||
$sResult .= $oNextLevel->spaceBeforeBlocks();
|
||||
if ($isFirst) {
|
||||
$isFirst = false;
|
||||
$result .= $nextLevelFormatter->spaceBeforeBlocks();
|
||||
} else {
|
||||
$sResult .= $oNextLevel->spaceBetweenBlocks();
|
||||
$result .= $nextLevelFormatter->spaceBetweenBlocks();
|
||||
}
|
||||
$sResult .= $sRendered;
|
||||
$result .= $renderedCss;
|
||||
}
|
||||
|
||||
if (!$bIsFirst) {
|
||||
if (!$isFirst) {
|
||||
// Had some output
|
||||
$sResult .= $oOutputFormat->spaceAfterBlocks();
|
||||
$result .= $formatter->spaceAfterBlocks();
|
||||
}
|
||||
|
||||
return $sResult;
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the list can not be further outdented. Only important when rendering.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
abstract public function isRootList();
|
||||
abstract public function isRootList(): bool;
|
||||
|
||||
/**
|
||||
* Returns the stored items.
|
||||
*
|
||||
* @return array<int, RuleSet|Import|Charset|CSSList>
|
||||
* @return array<int<0, max>, CSSListItem>
|
||||
*/
|
||||
public function getContents()
|
||||
public function getContents(): array
|
||||
{
|
||||
return $this->aContents;
|
||||
return $this->contents;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<array-key, Comment> $aComments
|
||||
* @return array<string, bool|int|float|string|array<mixed>|null>
|
||||
*
|
||||
* @return void
|
||||
* @internal
|
||||
*/
|
||||
public function addComments(array $aComments)
|
||||
public function getArrayRepresentation(): array
|
||||
{
|
||||
$this->aComments = array_merge($this->aComments, $aComments);
|
||||
throw new \BadMethodCallException('`getArrayRepresentation` is not yet implemented for `' . self::class . '`');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<array-key, Comment>
|
||||
* @param list<Selector> $selectors1
|
||||
* @param list<Selector> $selectors2
|
||||
*/
|
||||
public function getComments()
|
||||
private static function selectorsMatch(array $selectors1, array $selectors2): bool
|
||||
{
|
||||
return $this->aComments;
|
||||
$selectorStrings1 = self::getSelectorStrings($selectors1);
|
||||
$selectorStrings2 = self::getSelectorStrings($selectors2);
|
||||
|
||||
\sort($selectorStrings1);
|
||||
\sort($selectorStrings2);
|
||||
|
||||
return $selectorStrings1 === $selectorStrings2;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<array-key, Comment> $aComments
|
||||
* @param list<Selector> $selectors
|
||||
*
|
||||
* @return void
|
||||
* @return list<string>
|
||||
*/
|
||||
public function setComments(array $aComments)
|
||||
private static function getSelectorStrings(array $selectors): array
|
||||
{
|
||||
$this->aComments = $aComments;
|
||||
return \array_map(
|
||||
static function (Selector $selector): string {
|
||||
return $selector->getSelector();
|
||||
},
|
||||
$selectors
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sabberworm\CSS\CSSList;
|
||||
|
||||
use Sabberworm\CSS\Comment\Commentable;
|
||||
use Sabberworm\CSS\Renderable;
|
||||
|
||||
/**
|
||||
* Represents anything that can be in the `$contents` of a `CSSList`.
|
||||
*
|
||||
* The interface does not define any methods to implement.
|
||||
* It's purpose is to allow a single type to be specified for `CSSList::$contents` and manipulation methods thereof.
|
||||
* It extends `Commentable` and `Renderable` because all `CSSListItem`s are both.
|
||||
* This allows implementations to call methods from those interfaces without any additional type checks.
|
||||
*/
|
||||
interface CSSListItem extends Commentable, Renderable {}
|
||||
+16
-102
@@ -1,13 +1,13 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* This class represents the root of a parsed CSS file. It contains all top-level CSS contents: mostly declaration
|
||||
@@ -16,64 +16,16 @@ use Sabberworm\CSS\RuleSet\RuleSet;
|
||||
class Document extends CSSBlockList
|
||||
{
|
||||
/**
|
||||
* @param int $iLineNo
|
||||
*/
|
||||
public function __construct($iLineNo = 0)
|
||||
{
|
||||
parent::__construct($iLineNo);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Document
|
||||
*
|
||||
* @throws SourceException
|
||||
*
|
||||
* @internal since V8.8.0
|
||||
*/
|
||||
public static function parse(ParserState $oParserState)
|
||||
public static function parse(ParserState $parserState): Document
|
||||
{
|
||||
$oDocument = new Document($oParserState->currentLine());
|
||||
CSSList::parseList($oParserState, $oDocument);
|
||||
return $oDocument;
|
||||
}
|
||||
$document = new Document($parserState->currentLine());
|
||||
CSSList::parseList($parserState, $document);
|
||||
|
||||
/**
|
||||
* Gets all `DeclarationBlock` objects recursively, no matter how deeply nested the selectors are.
|
||||
* Aliased as `getAllSelectors()`.
|
||||
*
|
||||
* @return array<int, DeclarationBlock>
|
||||
*/
|
||||
public function getAllDeclarationBlocks()
|
||||
{
|
||||
/** @var array<int, DeclarationBlock> $aResult */
|
||||
$aResult = [];
|
||||
$this->allDeclarationBlocks($aResult);
|
||||
return $aResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all `DeclarationBlock` objects recursively.
|
||||
*
|
||||
* @return array<int, DeclarationBlock>
|
||||
*
|
||||
* @deprecated will be removed in version 9.0; use `getAllDeclarationBlocks()` instead
|
||||
*/
|
||||
public function getAllSelectors()
|
||||
{
|
||||
return $this->getAllDeclarationBlocks();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all `RuleSet` objects recursively found in the tree, no matter how deeply nested the rule sets are.
|
||||
*
|
||||
* @return array<int, RuleSet>
|
||||
*/
|
||||
public function getAllRuleSets()
|
||||
{
|
||||
/** @var array<int, RuleSet> $aResult */
|
||||
$aResult = [];
|
||||
$this->allRuleSets($aResult);
|
||||
return $aResult;
|
||||
return $document;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -82,69 +34,31 @@ class Document extends CSSBlockList
|
||||
* 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
|
||||
* @param string|null $specificitySearch
|
||||
* An optional filter by specificity.
|
||||
* May contain a comparison operator and a number or just a number (defaults to "==").
|
||||
*
|
||||
* @return array<int, Selector>
|
||||
* @return list<Selector>
|
||||
*
|
||||
* @example `getSelectorsBySpecificity('>= 100')`
|
||||
*
|
||||
*/
|
||||
public function getSelectorsBySpecificity($sSpecificitySearch = null)
|
||||
public function getSelectorsBySpecificity(?string $specificitySearch = null): array
|
||||
{
|
||||
/** @var array<int, Selector> $aResult */
|
||||
$aResult = [];
|
||||
$this->allSelectors($aResult, $sSpecificitySearch);
|
||||
return $aResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* Expands all shorthand properties to their long value.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @deprecated since 8.7.0, will be removed without substitution in version 9.0 in #511
|
||||
*/
|
||||
public function expandShorthands()
|
||||
{
|
||||
foreach ($this->getAllDeclarationBlocks() as $oDeclaration) {
|
||||
$oDeclaration->expandShorthands();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create shorthands properties whenever possible.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @deprecated since 8.7.0, will be removed without substitution in version 9.0 in #511
|
||||
*/
|
||||
public function createShorthands()
|
||||
{
|
||||
foreach ($this->getAllDeclarationBlocks() as $oDeclaration) {
|
||||
$oDeclaration->createShorthands();
|
||||
}
|
||||
return $this->getAllSelectors($specificitySearch);
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides `render()` to make format argument optional.
|
||||
*
|
||||
* @param OutputFormat|null $oOutputFormat
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function render($oOutputFormat = null)
|
||||
public function render(?OutputFormat $outputFormat = null): string
|
||||
{
|
||||
if ($oOutputFormat === null) {
|
||||
$oOutputFormat = new OutputFormat();
|
||||
if ($outputFormat === null) {
|
||||
$outputFormat = new OutputFormat();
|
||||
}
|
||||
return $oOutputFormat->comments($this) . $this->renderListContents($oOutputFormat);
|
||||
return $outputFormat->getFormatter()->comments($this) . $this->renderListContents($outputFormat);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isRootList()
|
||||
public function isRootList(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
+27
-49
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sabberworm\CSS\CSSList;
|
||||
|
||||
use Sabberworm\CSS\OutputFormat;
|
||||
@@ -8,101 +10,77 @@ use Sabberworm\CSS\Property\AtRule;
|
||||
class KeyFrame extends CSSList implements AtRule
|
||||
{
|
||||
/**
|
||||
* @var string|null
|
||||
* @var non-empty-string
|
||||
*/
|
||||
private $vendorKeyFrame;
|
||||
private $vendorKeyFrame = 'keyframes';
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
* @var non-empty-string
|
||||
*/
|
||||
private $animationName;
|
||||
private $animationName = 'none';
|
||||
|
||||
/**
|
||||
* @param int $iLineNo
|
||||
* @param non-empty-string $vendorKeyFrame
|
||||
*/
|
||||
public function __construct($iLineNo = 0)
|
||||
{
|
||||
parent::__construct($iLineNo);
|
||||
$this->vendorKeyFrame = null;
|
||||
$this->animationName = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $vendorKeyFrame
|
||||
*/
|
||||
public function setVendorKeyFrame($vendorKeyFrame)
|
||||
public function setVendorKeyFrame(string $vendorKeyFrame): void
|
||||
{
|
||||
$this->vendorKeyFrame = $vendorKeyFrame;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
* @return non-empty-string
|
||||
*/
|
||||
public function getVendorKeyFrame()
|
||||
public function getVendorKeyFrame(): string
|
||||
{
|
||||
return $this->vendorKeyFrame;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $animationName
|
||||
* @param non-empty-string $animationName
|
||||
*/
|
||||
public function setAnimationName($animationName)
|
||||
public function setAnimationName(string $animationName): void
|
||||
{
|
||||
$this->animationName = $animationName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
* @return non-empty-string
|
||||
*/
|
||||
public function getAnimationName()
|
||||
public function getAnimationName(): string
|
||||
{
|
||||
return $this->animationName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*
|
||||
* @deprecated in V8.8.0, will be removed in V9.0.0. Use `render` instead.
|
||||
* @return non-empty-string
|
||||
*/
|
||||
public function __toString()
|
||||
public function render(OutputFormat $outputFormat): string
|
||||
{
|
||||
return $this->render(new OutputFormat());
|
||||
$formatter = $outputFormat->getFormatter();
|
||||
$result = $formatter->comments($this);
|
||||
$result .= "@{$this->vendorKeyFrame} {$this->animationName}{$formatter->spaceBeforeOpeningBrace()}{";
|
||||
$result .= $this->renderListContents($outputFormat);
|
||||
$result .= '}';
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param OutputFormat|null $oOutputFormat
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function render($oOutputFormat)
|
||||
{
|
||||
$sResult = $oOutputFormat->comments($this);
|
||||
$sResult .= "@{$this->vendorKeyFrame} {$this->animationName}{$oOutputFormat->spaceBeforeOpeningBrace()}{";
|
||||
$sResult .= $this->renderListContents($oOutputFormat);
|
||||
$sResult .= '}';
|
||||
return $sResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isRootList()
|
||||
public function isRootList(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
* @return non-empty-string
|
||||
*/
|
||||
public function atRuleName()
|
||||
public function atRuleName(): string
|
||||
{
|
||||
return $this->vendorKeyFrame;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
* @return non-empty-string
|
||||
*/
|
||||
public function atRuleArgs()
|
||||
public function atRuleArgs(): string
|
||||
{
|
||||
return $this->animationName;
|
||||
}
|
||||
|
||||
+32
-34
@@ -1,68 +1,66 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sabberworm\CSS\Comment;
|
||||
|
||||
use Sabberworm\CSS\OutputFormat;
|
||||
use Sabberworm\CSS\Renderable;
|
||||
use Sabberworm\CSS\Position\Position;
|
||||
use Sabberworm\CSS\Position\Positionable;
|
||||
use Sabberworm\CSS\Renderable;
|
||||
use Sabberworm\CSS\ShortClassNameProvider;
|
||||
|
||||
class Comment implements Positionable, Renderable
|
||||
{
|
||||
use Position;
|
||||
use ShortClassNameProvider;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
* @internal since 8.8.0
|
||||
*/
|
||||
protected $sComment;
|
||||
protected $commentText;
|
||||
|
||||
/**
|
||||
* @param string $sComment
|
||||
* @param int $iLineNo
|
||||
* @param int<1, max>|null $lineNumber
|
||||
*/
|
||||
public function __construct($sComment = '', $iLineNo = 0)
|
||||
public function __construct(string $commentText = '', ?int $lineNumber = null)
|
||||
{
|
||||
$this->sComment = $sComment;
|
||||
$this->setPosition($iLineNo);
|
||||
$this->commentText = $commentText;
|
||||
$this->setPosition($lineNumber);
|
||||
}
|
||||
|
||||
public function getComment(): string
|
||||
{
|
||||
return $this->commentText;
|
||||
}
|
||||
|
||||
public function setComment(string $commentText): void
|
||||
{
|
||||
$this->commentText = $commentText;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
* @return non-empty-string
|
||||
*/
|
||||
public function getComment()
|
||||
public function render(OutputFormat $outputFormat): string
|
||||
{
|
||||
return $this->sComment;
|
||||
return '/*' . $this->commentText . '*/';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $sComment
|
||||
* @return array<string, bool|int|float|string|array<mixed>|null>
|
||||
*
|
||||
* @return void
|
||||
* @internal
|
||||
*/
|
||||
public function setComment($sComment)
|
||||
public function getArrayRepresentation(): array
|
||||
{
|
||||
$this->sComment = $sComment;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*
|
||||
* @deprecated in V8.8.0, will be removed in V9.0.0. Use `render` instead.
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
return $this->render(new OutputFormat());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param OutputFormat|null $oOutputFormat
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function render($oOutputFormat)
|
||||
{
|
||||
return '/*' . $this->sComment . '*/';
|
||||
return [
|
||||
'class' => $this->getShortClassName(),
|
||||
// "contents" is the term used in the W3C specs:
|
||||
// https://www.w3.org/TR/CSS22/syndata.html#comments
|
||||
'contents' => $this->commentText,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sabberworm\CSS\Comment;
|
||||
|
||||
/**
|
||||
* Provides a standard reusable implementation of `Commentable`.
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @phpstan-require-implements Commentable
|
||||
*/
|
||||
trait CommentContainer
|
||||
{
|
||||
/**
|
||||
* @var list<Comment>
|
||||
*/
|
||||
protected $comments = [];
|
||||
|
||||
/**
|
||||
* @param list<Comment> $comments
|
||||
*/
|
||||
public function addComments(array $comments): void
|
||||
{
|
||||
$this->comments = \array_merge($this->comments, $comments);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<Comment>
|
||||
*/
|
||||
public function getComments(): array
|
||||
{
|
||||
return $this->comments;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<Comment> $comments
|
||||
*/
|
||||
public function setComments(array $comments): void
|
||||
{
|
||||
$this->comments = $comments;
|
||||
}
|
||||
}
|
||||
+11
-10
@@ -1,25 +1,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sabberworm\CSS\Comment;
|
||||
|
||||
/**
|
||||
* A standard implementation of this interface is available in the `CommentContainer` trait.
|
||||
*/
|
||||
interface Commentable
|
||||
{
|
||||
/**
|
||||
* @param array<array-key, Comment> $aComments
|
||||
*
|
||||
* @return void
|
||||
* @param list<Comment> $comments
|
||||
*/
|
||||
public function addComments(array $aComments);
|
||||
public function addComments(array $comments): void;
|
||||
|
||||
/**
|
||||
* @return array<array-key, Comment>
|
||||
* @return list<Comment>
|
||||
*/
|
||||
public function getComments();
|
||||
public function getComments(): array;
|
||||
|
||||
/**
|
||||
* @param array<array-key, Comment> $aComments
|
||||
*
|
||||
* @return void
|
||||
* @param list<Comment> $comments
|
||||
*/
|
||||
public function setComments(array $aComments);
|
||||
public function setComments(array $comments): void;
|
||||
}
|
||||
|
||||
+556
-215
@@ -1,32 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sabberworm\CSS;
|
||||
|
||||
/**
|
||||
* Extending this class is deprecated in version 8.8.0; it will be made `final` in version 9.0.0.
|
||||
*
|
||||
* @method OutputFormat setSemicolonAfterLastRule(bool $bSemicolonAfterLastRule) Set whether semicolons are added after
|
||||
* last rule.
|
||||
*/
|
||||
class OutputFormat
|
||||
final class OutputFormat
|
||||
{
|
||||
/**
|
||||
* Value format: `"` means double-quote, `'` means single-quote
|
||||
*
|
||||
* @var string
|
||||
*
|
||||
* @internal since 8.8.0, will be made private in 9.0.0
|
||||
* @var '"'|"'"
|
||||
*/
|
||||
public $sStringQuotingType = '"';
|
||||
private $stringQuotingType = '"';
|
||||
|
||||
/**
|
||||
* Output RGB colors in hash notation if possible
|
||||
*
|
||||
* @var bool
|
||||
*
|
||||
* @internal since 8.8.0, will be made private in 9.0.0
|
||||
*/
|
||||
public $bRGBHashNotation = true;
|
||||
private $usesRgbHashNotation = true;
|
||||
|
||||
/**
|
||||
* Declaration format
|
||||
@@ -34,404 +24,755 @@ class OutputFormat
|
||||
* Semicolon after the last rule of a declaration block can be omitted. To do that, set this false.
|
||||
*
|
||||
* @var bool
|
||||
*
|
||||
* @internal since 8.8.0, will be made private in 9.0.0
|
||||
*/
|
||||
public $bSemicolonAfterLastRule = true;
|
||||
private $renderSemicolonAfterLastRule = 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");`)
|
||||
* The triples (After, Before, Between) can be set using a wildcard
|
||||
* (e.g. `$outputFormat->set('Space*Rules', "\n");`)
|
||||
*
|
||||
* @var string
|
||||
*
|
||||
* @internal since 8.8.0, will be made private in 9.0.0
|
||||
*/
|
||||
public $sSpaceAfterRuleName = ' ';
|
||||
private $spaceAfterRuleName = ' ';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
* @internal since 8.8.0, will be made private in 9.0.0
|
||||
*/
|
||||
public $sSpaceBeforeRules = '';
|
||||
private $spaceBeforeRules = '';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
* @internal since 8.8.0, will be made private in 9.0.0
|
||||
*/
|
||||
public $sSpaceAfterRules = '';
|
||||
private $spaceAfterRules = '';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
* @internal since 8.8.0, will be made private in 9.0.0
|
||||
*/
|
||||
public $sSpaceBetweenRules = '';
|
||||
private $spaceBetweenRules = '';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
* @internal since 8.8.0, will be made private in 9.0.0
|
||||
*/
|
||||
public $sSpaceBeforeBlocks = '';
|
||||
private $spaceBeforeBlocks = '';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
* @internal since 8.8.0, will be made private in 9.0.0
|
||||
*/
|
||||
public $sSpaceAfterBlocks = '';
|
||||
private $spaceAfterBlocks = '';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
* @internal since 8.8.0, will be made private in 9.0.0
|
||||
*/
|
||||
public $sSpaceBetweenBlocks = "\n";
|
||||
private $spaceBetweenBlocks = "\n";
|
||||
|
||||
/**
|
||||
* Content injected in and around at-rule blocks.
|
||||
*
|
||||
* @var string
|
||||
*
|
||||
* @internal since 8.8.0, will be made private in 9.0.0
|
||||
*/
|
||||
public $sBeforeAtRuleBlock = '';
|
||||
private $contentBeforeAtRuleBlock = '';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
* @internal since 8.8.0, will be made private in 9.0.0
|
||||
*/
|
||||
public $sAfterAtRuleBlock = '';
|
||||
private $contentAfterAtRuleBlock = '';
|
||||
|
||||
/**
|
||||
* This is what’s printed before and after the comma if a declaration block contains multiple selectors.
|
||||
*
|
||||
* @var string
|
||||
*
|
||||
* @internal since 8.8.0, will be made private in 9.0.0
|
||||
*/
|
||||
public $sSpaceBeforeSelectorSeparator = '';
|
||||
private $spaceBeforeSelectorSeparator = '';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
* @internal since 8.8.0, will be made private in 9.0.0
|
||||
*/
|
||||
public $sSpaceAfterSelectorSeparator = ' ';
|
||||
private $spaceAfterSelectorSeparator = ' ';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $spaceAroundSelectorCombinator = ' ';
|
||||
|
||||
/**
|
||||
* This is what’s inserted before the separator in value lists, by default.
|
||||
*
|
||||
* `array` is deprecated in version 8.8.0, and will be removed in version 9.0.0.
|
||||
* To set the spacing for specific separators, use {@see $aSpaceBeforeListArgumentSeparators} instead.
|
||||
*
|
||||
* @var string|array<non-empty-string, string>
|
||||
*
|
||||
* @internal since 8.8.0, will be made private in 9.0.0
|
||||
* @var string
|
||||
*/
|
||||
public $sSpaceBeforeListArgumentSeparator = '';
|
||||
private $spaceBeforeListArgumentSeparator = '';
|
||||
|
||||
/**
|
||||
* Keys are separators (e.g. `,`). Values are the space sequence to insert, or an empty string.
|
||||
*
|
||||
* @var array<non-empty-string, string>
|
||||
*
|
||||
* @internal since 8.8.0, will be made private in 9.0.0
|
||||
*/
|
||||
public $aSpaceBeforeListArgumentSeparators = [];
|
||||
private $spaceBeforeListArgumentSeparators = [];
|
||||
|
||||
/**
|
||||
* This is what’s inserted after the separator in value lists, by default.
|
||||
*
|
||||
* `array` is deprecated in version 8.8.0, and will be removed in version 9.0.0.
|
||||
* To set the spacing for specific separators, use {@see $aSpaceAfterListArgumentSeparators} instead.
|
||||
*
|
||||
* @var string|array<non-empty-string, string>
|
||||
*
|
||||
* @internal since 8.8.0, will be made private in 9.0.0
|
||||
* @var string
|
||||
*/
|
||||
public $sSpaceAfterListArgumentSeparator = '';
|
||||
private $spaceAfterListArgumentSeparator = '';
|
||||
|
||||
/**
|
||||
* Keys are separators (e.g. `,`). Values are the space sequence to insert, or an empty string.
|
||||
*
|
||||
* @var array<non-empty-string, string>
|
||||
*
|
||||
* @internal since 8.8.0, will be made private in 9.0.0
|
||||
*/
|
||||
public $aSpaceAfterListArgumentSeparators = [];
|
||||
private $spaceAfterListArgumentSeparators = [];
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
* @internal since 8.8.0, will be made private in 9.0.0
|
||||
*/
|
||||
public $sSpaceBeforeOpeningBrace = ' ';
|
||||
private $spaceBeforeOpeningBrace = ' ';
|
||||
|
||||
/**
|
||||
* Content injected in and around declaration blocks.
|
||||
*
|
||||
* @var string
|
||||
*
|
||||
* @internal since 8.8.0, will be made private in 9.0.0
|
||||
*/
|
||||
public $sBeforeDeclarationBlock = '';
|
||||
private $contentBeforeDeclarationBlock = '';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
* @internal since 8.8.0, will be made private in 9.0.0
|
||||
*/
|
||||
public $sAfterDeclarationBlockSelectors = '';
|
||||
private $contentAfterDeclarationBlockSelectors = '';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
* @internal since 8.8.0, will be made private in 9.0.0
|
||||
*/
|
||||
public $sAfterDeclarationBlock = '';
|
||||
private $contentAfterDeclarationBlock = '';
|
||||
|
||||
/**
|
||||
* Indentation character(s) per level. Only applicable if newlines are used in any of the spacing settings.
|
||||
*
|
||||
* @var string
|
||||
*
|
||||
* @internal since 8.8.0, will be made private in 9.0.0
|
||||
*/
|
||||
public $sIndentation = "\t";
|
||||
private $indentation = "\t";
|
||||
|
||||
/**
|
||||
* Output exceptions.
|
||||
*
|
||||
* @var bool
|
||||
*
|
||||
* @internal since 8.8.0, will be made private in 9.0.0
|
||||
*/
|
||||
public $bIgnoreExceptions = false;
|
||||
private $shouldIgnoreExceptions = false;
|
||||
|
||||
/**
|
||||
* Render comments for lists and RuleSets
|
||||
*
|
||||
* @var bool
|
||||
*
|
||||
* @internal since 8.8.0, will be made private in 9.0.0
|
||||
*/
|
||||
public $bRenderComments = false;
|
||||
private $shouldRenderComments = false;
|
||||
|
||||
/**
|
||||
* @var OutputFormatter|null
|
||||
*/
|
||||
private $oFormatter = null;
|
||||
private $outputFormatter;
|
||||
|
||||
/**
|
||||
* @var OutputFormat|null
|
||||
*/
|
||||
private $oNextLevelFormat = null;
|
||||
private $nextLevelFormat;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
* @var int<0, max>
|
||||
*/
|
||||
private $iIndentationLevel = 0;
|
||||
private $indentationLevel = 0;
|
||||
|
||||
/**
|
||||
* @internal since V8.8.0. Use the factory methods `create()`, `createCompact()`, or `createPretty()` instead.
|
||||
* @return '"'|"'"
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public function __construct()
|
||||
public function getStringQuotingType(): string
|
||||
{
|
||||
return $this->stringQuotingType;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $sName
|
||||
* @param '"'|"'" $quotingType
|
||||
*
|
||||
* @return string|null
|
||||
*
|
||||
* @deprecated since 8.8.0, will be removed in 9.0.0. Use specific getters instead.
|
||||
* @return $this fluent interface
|
||||
*/
|
||||
public function get($sName)
|
||||
public function setStringQuotingType(string $quotingType): self
|
||||
{
|
||||
$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;
|
||||
$this->stringQuotingType = $quotingType;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<array-key, string>|string $aNames
|
||||
* @param mixed $mValue
|
||||
*
|
||||
* @return self|false
|
||||
*
|
||||
* @deprecated since 8.8.0, will be removed in 9.0.0. Use specific setters instead.
|
||||
* @internal
|
||||
*/
|
||||
public function set($aNames, $mValue)
|
||||
public function usesRgbHashNotation(): bool
|
||||
{
|
||||
$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;
|
||||
return $this->usesRgbHashNotation;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $sMethodName
|
||||
* @param array<array-key, mixed> $aArguments
|
||||
*
|
||||
* @return mixed
|
||||
*
|
||||
* @throws \Exception
|
||||
* @return $this fluent interface
|
||||
*/
|
||||
public function __call($sMethodName, array $aArguments)
|
||||
public function setRGBHashNotation(bool $usesRgbHashNotation): self
|
||||
{
|
||||
if (strpos($sMethodName, 'set') === 0) {
|
||||
return $this->set(substr($sMethodName, 3), $aArguments[0]);
|
||||
} elseif (strpos($sMethodName, 'get') === 0) {
|
||||
return $this->get(substr($sMethodName, 3));
|
||||
} elseif (method_exists(OutputFormatter::class, $sMethodName)) {
|
||||
// @deprecated since 8.8.0, will be removed in 9.0.0. Call the method on the formatter directly instead.
|
||||
return call_user_func_array([$this->getFormatter(), $sMethodName], $aArguments);
|
||||
} else {
|
||||
throw new \Exception('Unknown OutputFormat method called: ' . $sMethodName);
|
||||
}
|
||||
$this->usesRgbHashNotation = $usesRgbHashNotation;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $iNumber
|
||||
*
|
||||
* @return self
|
||||
* @internal
|
||||
*/
|
||||
public function indentWithTabs($iNumber = 1)
|
||||
public function shouldRenderSemicolonAfterLastRule(): bool
|
||||
{
|
||||
return $this->setIndentation(str_repeat("\t", $iNumber));
|
||||
return $this->renderSemicolonAfterLastRule;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $iNumber
|
||||
*
|
||||
* @return self
|
||||
* @return $this fluent interface
|
||||
*/
|
||||
public function indentWithSpaces($iNumber = 2)
|
||||
public function setSemicolonAfterLastRule(bool $renderSemicolonAfterLastRule): self
|
||||
{
|
||||
return $this->setIndentation(str_repeat(" ", $iNumber));
|
||||
$this->renderSemicolonAfterLastRule = $renderSemicolonAfterLastRule;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return OutputFormat
|
||||
* @internal
|
||||
*/
|
||||
public function getSpaceAfterRuleName(): string
|
||||
{
|
||||
return $this->spaceAfterRuleName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this fluent interface
|
||||
*/
|
||||
public function setSpaceAfterRuleName(string $whitespace): self
|
||||
{
|
||||
$this->spaceAfterRuleName = $whitespace;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function getSpaceBeforeRules(): string
|
||||
{
|
||||
return $this->spaceBeforeRules;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this fluent interface
|
||||
*/
|
||||
public function setSpaceBeforeRules(string $whitespace): self
|
||||
{
|
||||
$this->spaceBeforeRules = $whitespace;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function getSpaceAfterRules(): string
|
||||
{
|
||||
return $this->spaceAfterRules;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this fluent interface
|
||||
*/
|
||||
public function setSpaceAfterRules(string $whitespace): self
|
||||
{
|
||||
$this->spaceAfterRules = $whitespace;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function getSpaceBetweenRules(): string
|
||||
{
|
||||
return $this->spaceBetweenRules;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this fluent interface
|
||||
*/
|
||||
public function setSpaceBetweenRules(string $whitespace): self
|
||||
{
|
||||
$this->spaceBetweenRules = $whitespace;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function getSpaceBeforeBlocks(): string
|
||||
{
|
||||
return $this->spaceBeforeBlocks;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this fluent interface
|
||||
*/
|
||||
public function setSpaceBeforeBlocks(string $whitespace): self
|
||||
{
|
||||
$this->spaceBeforeBlocks = $whitespace;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function getSpaceAfterBlocks(): string
|
||||
{
|
||||
return $this->spaceAfterBlocks;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this fluent interface
|
||||
*/
|
||||
public function setSpaceAfterBlocks(string $whitespace): self
|
||||
{
|
||||
$this->spaceAfterBlocks = $whitespace;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function getSpaceBetweenBlocks(): string
|
||||
{
|
||||
return $this->spaceBetweenBlocks;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this fluent interface
|
||||
*/
|
||||
public function setSpaceBetweenBlocks(string $whitespace): self
|
||||
{
|
||||
$this->spaceBetweenBlocks = $whitespace;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function getContentBeforeAtRuleBlock(): string
|
||||
{
|
||||
return $this->contentBeforeAtRuleBlock;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this fluent interface
|
||||
*/
|
||||
public function setBeforeAtRuleBlock(string $content): self
|
||||
{
|
||||
$this->contentBeforeAtRuleBlock = $content;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function getContentAfterAtRuleBlock(): string
|
||||
{
|
||||
return $this->contentAfterAtRuleBlock;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this fluent interface
|
||||
*/
|
||||
public function setAfterAtRuleBlock(string $content): self
|
||||
{
|
||||
$this->contentAfterAtRuleBlock = $content;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function getSpaceBeforeSelectorSeparator(): string
|
||||
{
|
||||
return $this->spaceBeforeSelectorSeparator;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this fluent interface
|
||||
*/
|
||||
public function setSpaceBeforeSelectorSeparator(string $whitespace): self
|
||||
{
|
||||
$this->spaceBeforeSelectorSeparator = $whitespace;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function getSpaceAfterSelectorSeparator(): string
|
||||
{
|
||||
return $this->spaceAfterSelectorSeparator;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this fluent interface
|
||||
*/
|
||||
public function setSpaceAfterSelectorSeparator(string $whitespace): self
|
||||
{
|
||||
$this->spaceAfterSelectorSeparator = $whitespace;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function getSpaceAroundSelectorCombinator(): string
|
||||
{
|
||||
return $this->spaceAroundSelectorCombinator;
|
||||
}
|
||||
|
||||
/**
|
||||
* The spacing set is also used for the descendent combinator, which is whitespace only,
|
||||
* unless an empty string is set, in which case a space will be used.
|
||||
*
|
||||
* @return $this fluent interface
|
||||
*/
|
||||
public function setSpaceAroundSelectorCombinator(string $whitespace): self
|
||||
{
|
||||
$this->spaceAroundSelectorCombinator = $whitespace;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function getSpaceBeforeListArgumentSeparator(): string
|
||||
{
|
||||
return $this->spaceBeforeListArgumentSeparator;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this fluent interface
|
||||
*/
|
||||
public function setSpaceBeforeListArgumentSeparator(string $whitespace): self
|
||||
{
|
||||
$this->spaceBeforeListArgumentSeparator = $whitespace;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<non-empty-string, string>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public function getSpaceBeforeListArgumentSeparators(): array
|
||||
{
|
||||
return $this->spaceBeforeListArgumentSeparators;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<non-empty-string, string> $separatorSpaces
|
||||
*
|
||||
* @return $this fluent interface
|
||||
*/
|
||||
public function setSpaceBeforeListArgumentSeparators(array $separatorSpaces): self
|
||||
{
|
||||
$this->spaceBeforeListArgumentSeparators = $separatorSpaces;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function getSpaceAfterListArgumentSeparator(): string
|
||||
{
|
||||
return $this->spaceAfterListArgumentSeparator;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this fluent interface
|
||||
*/
|
||||
public function setSpaceAfterListArgumentSeparator(string $whitespace): self
|
||||
{
|
||||
$this->spaceAfterListArgumentSeparator = $whitespace;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<non-empty-string, string>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public function getSpaceAfterListArgumentSeparators(): array
|
||||
{
|
||||
return $this->spaceAfterListArgumentSeparators;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<non-empty-string, string> $separatorSpaces
|
||||
*
|
||||
* @return $this fluent interface
|
||||
*/
|
||||
public function setSpaceAfterListArgumentSeparators(array $separatorSpaces): self
|
||||
{
|
||||
$this->spaceAfterListArgumentSeparators = $separatorSpaces;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function getSpaceBeforeOpeningBrace(): string
|
||||
{
|
||||
return $this->spaceBeforeOpeningBrace;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this fluent interface
|
||||
*/
|
||||
public function setSpaceBeforeOpeningBrace(string $whitespace): self
|
||||
{
|
||||
$this->spaceBeforeOpeningBrace = $whitespace;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function getContentBeforeDeclarationBlock(): string
|
||||
{
|
||||
return $this->contentBeforeDeclarationBlock;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this fluent interface
|
||||
*/
|
||||
public function setBeforeDeclarationBlock(string $content): self
|
||||
{
|
||||
$this->contentBeforeDeclarationBlock = $content;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function getContentAfterDeclarationBlockSelectors(): string
|
||||
{
|
||||
return $this->contentAfterDeclarationBlockSelectors;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this fluent interface
|
||||
*/
|
||||
public function setAfterDeclarationBlockSelectors(string $content): self
|
||||
{
|
||||
$this->contentAfterDeclarationBlockSelectors = $content;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function getContentAfterDeclarationBlock(): string
|
||||
{
|
||||
return $this->contentAfterDeclarationBlock;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this fluent interface
|
||||
*/
|
||||
public function setAfterDeclarationBlock(string $content): self
|
||||
{
|
||||
$this->contentAfterDeclarationBlock = $content;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function getIndentation(): string
|
||||
{
|
||||
return $this->indentation;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this fluent interface
|
||||
*/
|
||||
public function setIndentation(string $indentation): self
|
||||
{
|
||||
$this->indentation = $indentation;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function shouldIgnoreExceptions(): bool
|
||||
{
|
||||
return $this->shouldIgnoreExceptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this fluent interface
|
||||
*/
|
||||
public function setIgnoreExceptions(bool $ignoreExceptions): self
|
||||
{
|
||||
$this->shouldIgnoreExceptions = $ignoreExceptions;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function shouldRenderComments(): bool
|
||||
{
|
||||
return $this->shouldRenderComments;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this fluent interface
|
||||
*/
|
||||
public function setRenderComments(bool $renderComments): self
|
||||
{
|
||||
$this->shouldRenderComments = $renderComments;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int<0, max>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public function getIndentationLevel(): int
|
||||
{
|
||||
return $this->indentationLevel;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int<1, max> $numberOfTabs
|
||||
*
|
||||
* @return $this fluent interface
|
||||
*/
|
||||
public function indentWithTabs(int $numberOfTabs = 1): self
|
||||
{
|
||||
return $this->setIndentation(\str_repeat("\t", $numberOfTabs));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int<1, max> $numberOfSpaces
|
||||
*
|
||||
* @return $this fluent interface
|
||||
*/
|
||||
public function indentWithSpaces(int $numberOfSpaces = 2): self
|
||||
{
|
||||
return $this->setIndentation(\str_repeat(' ', $numberOfSpaces));
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal since V8.8.0
|
||||
*/
|
||||
public function nextLevel()
|
||||
public function nextLevel(): self
|
||||
{
|
||||
if ($this->oNextLevelFormat === null) {
|
||||
$this->oNextLevelFormat = clone $this;
|
||||
$this->oNextLevelFormat->iIndentationLevel++;
|
||||
$this->oNextLevelFormat->oFormatter = null;
|
||||
if ($this->nextLevelFormat === null) {
|
||||
$this->nextLevelFormat = clone $this;
|
||||
$this->nextLevelFormat->indentationLevel++;
|
||||
$this->nextLevelFormat->outputFormatter = null;
|
||||
}
|
||||
return $this->oNextLevelFormat;
|
||||
return $this->nextLevelFormat;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function beLenient()
|
||||
public function beLenient(): void
|
||||
{
|
||||
$this->bIgnoreExceptions = true;
|
||||
$this->shouldIgnoreExceptions = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return OutputFormatter
|
||||
*
|
||||
* @internal since 8.8.0
|
||||
*/
|
||||
public function getFormatter()
|
||||
public function getFormatter(): OutputFormatter
|
||||
{
|
||||
if ($this->oFormatter === null) {
|
||||
$this->oFormatter = new OutputFormatter($this);
|
||||
if ($this->outputFormatter === null) {
|
||||
$this->outputFormatter = new OutputFormatter($this);
|
||||
}
|
||||
|
||||
return $this->oFormatter;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*
|
||||
* @deprecated #869 since version V8.8.0, will be removed in V9.0.0. Use `getIndentationLevel()` instead.
|
||||
*/
|
||||
public function level()
|
||||
{
|
||||
return $this->iIndentationLevel;
|
||||
return $this->outputFormatter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an instance of this class without any particular formatting settings.
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public static function create()
|
||||
public static function create(): self
|
||||
{
|
||||
return new OutputFormat();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an instance of this class with a preset for compact formatting.
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public static function createCompact()
|
||||
public static function createCompact(): self
|
||||
{
|
||||
$format = self::create();
|
||||
$format->set('Space*Rules', "")
|
||||
->set('Space*Blocks', "")
|
||||
$format
|
||||
->setSpaceBeforeRules('')
|
||||
->setSpaceBetweenRules('')
|
||||
->setSpaceAfterRules('')
|
||||
->setSpaceBeforeBlocks('')
|
||||
->setSpaceBetweenBlocks('')
|
||||
->setSpaceAfterBlocks('')
|
||||
->setSpaceAfterRuleName('')
|
||||
->setSpaceBeforeOpeningBrace('')
|
||||
->setSpaceAfterSelectorSeparator('')
|
||||
->setSpaceAroundSelectorCombinator('')
|
||||
->setSemicolonAfterLastRule(false)
|
||||
->setRenderComments(false);
|
||||
|
||||
return $format;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an instance of this class with a preset for pretty formatting.
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public static function createPretty()
|
||||
public static function createPretty(): self
|
||||
{
|
||||
$format = self::create();
|
||||
$format->set('Space*Rules', "\n")
|
||||
->set('Space*Blocks', "\n")
|
||||
$format
|
||||
->setSpaceBeforeRules("\n")
|
||||
->setSpaceBetweenRules("\n")
|
||||
->setSpaceAfterRules("\n")
|
||||
->setSpaceBeforeBlocks("\n")
|
||||
->setSpaceBetweenBlocks("\n\n")
|
||||
->set('SpaceAfterListArgumentSeparators', [',' => ' '])
|
||||
->setSpaceAfterBlocks("\n")
|
||||
->setSpaceAfterListArgumentSeparators([',' => ' '])
|
||||
->setRenderComments(true);
|
||||
|
||||
return $format;
|
||||
}
|
||||
}
|
||||
|
||||
+113
-146
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sabberworm\CSS;
|
||||
|
||||
use Sabberworm\CSS\Comment\Commentable;
|
||||
@@ -13,256 +15,221 @@ class OutputFormatter
|
||||
/**
|
||||
* @var OutputFormat
|
||||
*/
|
||||
private $oFormat;
|
||||
private $outputFormat;
|
||||
|
||||
public function __construct(OutputFormat $oFormat)
|
||||
public function __construct(OutputFormat $outputFormat)
|
||||
{
|
||||
$this->oFormat = $oFormat;
|
||||
$this->outputFormat = $outputFormat;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $sName
|
||||
* @param string|null $sType
|
||||
* @param non-empty-string $name
|
||||
*
|
||||
* @return string
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function space($sName, $sType = null)
|
||||
public function space(string $name): string
|
||||
{
|
||||
$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);
|
||||
}
|
||||
switch ($name) {
|
||||
case 'AfterRuleName':
|
||||
$spaceString = $this->outputFormat->getSpaceAfterRuleName();
|
||||
break;
|
||||
case 'BeforeRules':
|
||||
$spaceString = $this->outputFormat->getSpaceBeforeRules();
|
||||
break;
|
||||
case 'AfterRules':
|
||||
$spaceString = $this->outputFormat->getSpaceAfterRules();
|
||||
break;
|
||||
case 'BetweenRules':
|
||||
$spaceString = $this->outputFormat->getSpaceBetweenRules();
|
||||
break;
|
||||
case 'BeforeBlocks':
|
||||
$spaceString = $this->outputFormat->getSpaceBeforeBlocks();
|
||||
break;
|
||||
case 'AfterBlocks':
|
||||
$spaceString = $this->outputFormat->getSpaceAfterBlocks();
|
||||
break;
|
||||
case 'BetweenBlocks':
|
||||
$spaceString = $this->outputFormat->getSpaceBetweenBlocks();
|
||||
break;
|
||||
case 'BeforeSelectorSeparator':
|
||||
$spaceString = $this->outputFormat->getSpaceBeforeSelectorSeparator();
|
||||
break;
|
||||
case 'AfterSelectorSeparator':
|
||||
$spaceString = $this->outputFormat->getSpaceAfterSelectorSeparator();
|
||||
break;
|
||||
case 'BeforeOpeningBrace':
|
||||
$spaceString = $this->outputFormat->getSpaceBeforeOpeningBrace();
|
||||
break;
|
||||
case 'BeforeListArgumentSeparator':
|
||||
$spaceString = $this->outputFormat->getSpaceBeforeListArgumentSeparator();
|
||||
break;
|
||||
case 'AfterListArgumentSeparator':
|
||||
$spaceString = $this->outputFormat->getSpaceAfterListArgumentSeparator();
|
||||
break;
|
||||
default:
|
||||
throw new \InvalidArgumentException("Unknown space type: $name", 1740049248);
|
||||
}
|
||||
return $this->prepareSpace($sSpaceString);
|
||||
|
||||
return $this->prepareSpace($spaceString);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function spaceAfterRuleName()
|
||||
public function spaceAfterRuleName(): string
|
||||
{
|
||||
return $this->space('AfterRuleName');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function spaceBeforeRules()
|
||||
public function spaceBeforeRules(): string
|
||||
{
|
||||
return $this->space('BeforeRules');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function spaceAfterRules()
|
||||
public function spaceAfterRules(): string
|
||||
{
|
||||
return $this->space('AfterRules');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function spaceBetweenRules()
|
||||
public function spaceBetweenRules(): string
|
||||
{
|
||||
return $this->space('BetweenRules');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function spaceBeforeBlocks()
|
||||
public function spaceBeforeBlocks(): string
|
||||
{
|
||||
return $this->space('BeforeBlocks');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function spaceAfterBlocks()
|
||||
public function spaceAfterBlocks(): string
|
||||
{
|
||||
return $this->space('AfterBlocks');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function spaceBetweenBlocks()
|
||||
public function spaceBetweenBlocks(): string
|
||||
{
|
||||
return $this->space('BetweenBlocks');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function spaceBeforeSelectorSeparator()
|
||||
public function spaceBeforeSelectorSeparator(): string
|
||||
{
|
||||
return $this->space('BeforeSelectorSeparator');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function spaceAfterSelectorSeparator()
|
||||
public function spaceAfterSelectorSeparator(): string
|
||||
{
|
||||
return $this->space('AfterSelectorSeparator');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $sSeparator
|
||||
*
|
||||
* @return string
|
||||
* @param non-empty-string $separator
|
||||
*/
|
||||
public function spaceBeforeListArgumentSeparator($sSeparator)
|
||||
public function spaceBeforeListArgumentSeparator(string $separator): string
|
||||
{
|
||||
$spaceForSeparator = $this->oFormat->getSpaceBeforeListArgumentSeparators();
|
||||
if (isset($spaceForSeparator[$sSeparator])) {
|
||||
return $spaceForSeparator[$sSeparator];
|
||||
}
|
||||
$spaceForSeparator = $this->outputFormat->getSpaceBeforeListArgumentSeparators();
|
||||
|
||||
return $this->space('BeforeListArgumentSeparator', $sSeparator);
|
||||
return $spaceForSeparator[$separator] ?? $this->space('BeforeListArgumentSeparator');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $sSeparator
|
||||
*
|
||||
* @return string
|
||||
* @param non-empty-string $separator
|
||||
*/
|
||||
public function spaceAfterListArgumentSeparator($sSeparator)
|
||||
public function spaceAfterListArgumentSeparator(string $separator): string
|
||||
{
|
||||
$spaceForSeparator = $this->oFormat->getSpaceAfterListArgumentSeparators();
|
||||
if (isset($spaceForSeparator[$sSeparator])) {
|
||||
return $spaceForSeparator[$sSeparator];
|
||||
}
|
||||
$spaceForSeparator = $this->outputFormat->getSpaceAfterListArgumentSeparators();
|
||||
|
||||
return $this->space('AfterListArgumentSeparator', $sSeparator);
|
||||
return $spaceForSeparator[$separator] ?? $this->space('AfterListArgumentSeparator');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function spaceBeforeOpeningBrace()
|
||||
public function spaceBeforeOpeningBrace(): string
|
||||
{
|
||||
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
|
||||
* Runs the given code, either swallowing or passing exceptions, depending on the `ignoreExceptions` setting.
|
||||
*/
|
||||
public function safely($cCode)
|
||||
public function safely(callable $callable): ?string
|
||||
{
|
||||
if ($this->oFormat->get('IgnoreExceptions')) {
|
||||
if ($this->outputFormat->shouldIgnoreExceptions()) {
|
||||
// If output exceptions are ignored, run the code with exception guards
|
||||
try {
|
||||
return $cCode();
|
||||
return $callable();
|
||||
} catch (OutputException $e) {
|
||||
return null;
|
||||
} // Do nothing
|
||||
} else {
|
||||
// Run the code as-is
|
||||
return $cCode();
|
||||
return $callable();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clone of the `implode` function, but calls `render` with the current output format instead of `__toString()`.
|
||||
* Clone of the `implode` function, but calls `render` with the current output format.
|
||||
*
|
||||
* @param string $sSeparator
|
||||
* @param array<array-key, Renderable|string> $aValues
|
||||
* @param bool $bIncreaseLevel
|
||||
*
|
||||
* @return string
|
||||
* @param array<array-key, Renderable|string> $values
|
||||
*/
|
||||
public function implode($sSeparator, array $aValues, $bIncreaseLevel = false)
|
||||
public function implode(string $separator, array $values, bool $increaseLevel = false): string
|
||||
{
|
||||
$sResult = '';
|
||||
$oFormat = $this->oFormat;
|
||||
if ($bIncreaseLevel) {
|
||||
$oFormat = $oFormat->nextLevel();
|
||||
$result = '';
|
||||
$outputFormat = $this->outputFormat;
|
||||
if ($increaseLevel) {
|
||||
$outputFormat = $outputFormat->nextLevel();
|
||||
}
|
||||
$bIsFirst = true;
|
||||
foreach ($aValues as $mValue) {
|
||||
if ($bIsFirst) {
|
||||
$bIsFirst = false;
|
||||
$isFirst = true;
|
||||
foreach ($values as $value) {
|
||||
if ($isFirst) {
|
||||
$isFirst = false;
|
||||
} else {
|
||||
$sResult .= $sSeparator;
|
||||
$result .= $separator;
|
||||
}
|
||||
if ($mValue instanceof Renderable) {
|
||||
$sResult .= $mValue->render($oFormat);
|
||||
if ($value instanceof Renderable) {
|
||||
$result .= $value->render($outputFormat);
|
||||
} else {
|
||||
$sResult .= $mValue;
|
||||
$result .= $value;
|
||||
}
|
||||
}
|
||||
return $sResult;
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $sString
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function removeLastSemicolon($sString)
|
||||
public function removeLastSemicolon(string $string): string
|
||||
{
|
||||
if ($this->oFormat->get('SemicolonAfterLastRule')) {
|
||||
return $sString;
|
||||
if ($this->outputFormat->shouldRenderSemicolonAfterLastRule()) {
|
||||
return $string;
|
||||
}
|
||||
$sString = explode(';', $sString);
|
||||
if (count($sString) < 2) {
|
||||
return $sString[0];
|
||||
|
||||
$parts = \explode(';', $string);
|
||||
if (\count($parts) < 2) {
|
||||
return $parts[0];
|
||||
}
|
||||
$sLast = array_pop($sString);
|
||||
$sNextToLast = array_pop($sString);
|
||||
array_push($sString, $sNextToLast . $sLast);
|
||||
return implode(';', $sString);
|
||||
$lastPart = \array_pop($parts);
|
||||
$nextToLastPart = \array_pop($parts);
|
||||
\array_push($parts, $nextToLastPart . $lastPart);
|
||||
|
||||
return \implode(';', $parts);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param array<Commentable> $aComments
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function comments(Commentable $oCommentable)
|
||||
public function comments(Commentable $commentable): string
|
||||
{
|
||||
if (!$this->oFormat->bRenderComments) {
|
||||
if (!$this->outputFormat->shouldRenderComments()) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$sResult = '';
|
||||
$aComments = $oCommentable->getComments();
|
||||
$iLastCommentIndex = count($aComments) - 1;
|
||||
$result = '';
|
||||
$comments = $commentable->getComments();
|
||||
$lastCommentIndex = \count($comments) - 1;
|
||||
|
||||
foreach ($aComments as $i => $oComment) {
|
||||
$sResult .= $oComment->render($this->oFormat);
|
||||
$sResult .= $i === $iLastCommentIndex ? $this->spaceAfterBlocks() : $this->spaceBetweenBlocks();
|
||||
foreach ($comments as $i => $comment) {
|
||||
$result .= $comment->render($this->outputFormat);
|
||||
$result .= $i === $lastCommentIndex ? $this->spaceAfterBlocks() : $this->spaceBetweenBlocks();
|
||||
}
|
||||
return $sResult;
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $sSpaceString
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function prepareSpace($sSpaceString)
|
||||
private function prepareSpace(string $spaceString): string
|
||||
{
|
||||
return str_replace("\n", "\n" . $this->indent(), $sSpaceString);
|
||||
return \str_replace("\n", "\n" . $this->indent(), $spaceString);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
private function indent()
|
||||
private function indent(): string
|
||||
{
|
||||
return str_repeat($this->oFormat->sIndentation, $this->oFormat->getIndentationLevel());
|
||||
return \str_repeat($this->outputFormat->getIndentation(), $this->outputFormat->getIndentationLevel());
|
||||
}
|
||||
}
|
||||
|
||||
+11
-39
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sabberworm\CSS;
|
||||
|
||||
use Sabberworm\CSS\CSSList\Document;
|
||||
@@ -14,57 +16,27 @@ class Parser
|
||||
/**
|
||||
* @var ParserState
|
||||
*/
|
||||
private $oParserState;
|
||||
private $parserState;
|
||||
|
||||
/**
|
||||
* @param string $sText the complete CSS as text (i.e., usually the contents of a CSS file)
|
||||
* @param Settings|null $oParserSettings
|
||||
* @param int $iLineNo the line number (starting from 1, not from 0)
|
||||
* @param string $text the complete CSS as text (i.e., usually the contents of a CSS file)
|
||||
* @param int<1, max> $lineNumber the line number (starting from 1, not from 0)
|
||||
*/
|
||||
public function __construct($sText, $oParserSettings = null, $iLineNo = 1)
|
||||
public function __construct(string $text, ?Settings $parserSettings = null, int $lineNumber = 1)
|
||||
{
|
||||
if ($oParserSettings === null) {
|
||||
$oParserSettings = Settings::create();
|
||||
if ($parserSettings === null) {
|
||||
$parserSettings = Settings::create();
|
||||
}
|
||||
$this->oParserState = new ParserState($sText, $oParserSettings, $iLineNo);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the charset to be used if the CSS does not contain an `@charset` declaration.
|
||||
*
|
||||
* @param string $sCharset
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @deprecated since 8.7.0, will be removed in version 9.0.0 with #687
|
||||
*/
|
||||
public function setCharset($sCharset)
|
||||
{
|
||||
$this->oParserState->setCharset($sCharset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the charset that is used if the CSS does not contain an `@charset` declaration.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @deprecated since 8.7.0, will be removed in version 9.0.0 with #687
|
||||
*/
|
||||
public function getCharset()
|
||||
{
|
||||
// Note: The `return` statement is missing here. This is a bug that needs to be fixed.
|
||||
$this->oParserState->getCharset();
|
||||
$this->parserState = new ParserState($text, $parserSettings, $lineNumber);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the CSS provided to the constructor and creates a `Document` from it.
|
||||
*
|
||||
* @return Document
|
||||
*
|
||||
* @throws SourceException
|
||||
*/
|
||||
public function parse()
|
||||
public function parse(): Document
|
||||
{
|
||||
return Document::parse($this->oParserState);
|
||||
return Document::parse($this->parserState);
|
||||
}
|
||||
}
|
||||
|
||||
+12
-14
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sabberworm\CSS\Parsing;
|
||||
|
||||
/**
|
||||
@@ -8,30 +10,26 @@ namespace Sabberworm\CSS\Parsing;
|
||||
class Anchor
|
||||
{
|
||||
/**
|
||||
* @var int
|
||||
* @var int<0, max>
|
||||
*/
|
||||
private $iPosition;
|
||||
private $position;
|
||||
|
||||
/**
|
||||
* @var \Sabberworm\CSS\Parsing\ParserState
|
||||
* @var ParserState
|
||||
*/
|
||||
private $oParserState;
|
||||
private $parserState;
|
||||
|
||||
/**
|
||||
* @param int $iPosition
|
||||
* @param \Sabberworm\CSS\Parsing\ParserState $oParserState
|
||||
* @param int<0, max> $position
|
||||
*/
|
||||
public function __construct($iPosition, ParserState $oParserState)
|
||||
public function __construct(int $position, ParserState $parserState)
|
||||
{
|
||||
$this->iPosition = $iPosition;
|
||||
$this->oParserState = $oParserState;
|
||||
$this->position = $position;
|
||||
$this->parserState = $parserState;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function backtrack()
|
||||
public function backtrack(): void
|
||||
{
|
||||
$this->oParserState->setPosition($this->iPosition);
|
||||
$this->parserState->setPosition($this->position);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,10 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
final class OutputException extends SourceException {}
|
||||
|
||||
+254
-305
@@ -1,220 +1,173 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sabberworm\CSS\Parsing;
|
||||
|
||||
use Sabberworm\CSS\Comment\Comment;
|
||||
use Sabberworm\CSS\Settings;
|
||||
|
||||
use function Safe\iconv;
|
||||
use function Safe\preg_match;
|
||||
use function Safe\preg_split;
|
||||
|
||||
/**
|
||||
* @internal since 8.7.0
|
||||
*/
|
||||
class ParserState
|
||||
{
|
||||
/**
|
||||
* @var null
|
||||
*
|
||||
* @internal since 8.5.2
|
||||
*/
|
||||
const EOF = null;
|
||||
public const EOF = null;
|
||||
|
||||
/**
|
||||
* @var Settings
|
||||
*/
|
||||
private $oParserSettings;
|
||||
private $parserSettings;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $sText;
|
||||
private $text;
|
||||
|
||||
/**
|
||||
* @var array<int, string>
|
||||
*/
|
||||
private $aText;
|
||||
private $characters;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
* @var int<0, max>
|
||||
*/
|
||||
private $iCurrentPosition;
|
||||
private $currentPosition = 0;
|
||||
|
||||
/**
|
||||
* will only be used if the CSS does not contain an `@charset` declaration
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $sCharset;
|
||||
private $charset;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
* @var int<1, max> $lineNumber
|
||||
*/
|
||||
private $iLength;
|
||||
private $lineNumber;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
* @param string $text the complete CSS as text (i.e., usually the contents of a CSS file)
|
||||
* @param int<1, max> $lineNumber
|
||||
*/
|
||||
private $iLineNo;
|
||||
|
||||
/**
|
||||
* @param string $sText the complete CSS as text (i.e., usually the contents of a CSS file)
|
||||
* @param int $iLineNo
|
||||
*/
|
||||
public function __construct($sText, Settings $oParserSettings, $iLineNo = 1)
|
||||
public function __construct(string $text, Settings $parserSettings, int $lineNumber = 1)
|
||||
{
|
||||
$this->oParserSettings = $oParserSettings;
|
||||
$this->sText = $sText;
|
||||
$this->iCurrentPosition = 0;
|
||||
$this->iLineNo = $iLineNo;
|
||||
$this->setCharset($this->oParserSettings->sDefaultCharset);
|
||||
$this->parserSettings = $parserSettings;
|
||||
$this->text = $text;
|
||||
$this->lineNumber = $lineNumber;
|
||||
$this->setCharset($this->parserSettings->getDefaultCharset());
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the charset to be used if the CSS does not contain an `@charset` declaration.
|
||||
*
|
||||
* @param string $sCharset
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setCharset($sCharset)
|
||||
public function setCharset(string $charset): void
|
||||
{
|
||||
$this->sCharset = $sCharset;
|
||||
$this->aText = $this->strsplit($this->sText);
|
||||
if (is_array($this->aText)) {
|
||||
$this->iLength = count($this->aText);
|
||||
}
|
||||
$this->charset = $charset;
|
||||
$this->characters = $this->strsplit($this->text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the charset that is used if the CSS does not contain an `@charset` declaration.
|
||||
*
|
||||
* @return string
|
||||
* @return int<1, max>
|
||||
*/
|
||||
public function getCharset()
|
||||
public function currentLine(): int
|
||||
{
|
||||
return $this->sCharset;
|
||||
return $this->lineNumber;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
* @return int<0, max>
|
||||
*/
|
||||
public function currentLine()
|
||||
public function currentColumn(): int
|
||||
{
|
||||
return $this->iLineNo;
|
||||
return $this->currentPosition;
|
||||
}
|
||||
|
||||
public function getSettings(): Settings
|
||||
{
|
||||
return $this->parserSettings;
|
||||
}
|
||||
|
||||
public function anchor(): Anchor
|
||||
{
|
||||
return new Anchor($this->currentPosition, $this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
* @param int<0, max> $position
|
||||
*/
|
||||
public function currentColumn()
|
||||
public function setPosition(int $position): void
|
||||
{
|
||||
return $this->iCurrentPosition;
|
||||
$this->currentPosition = $position;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Settings
|
||||
*/
|
||||
public function getSettings()
|
||||
{
|
||||
return $this->oParserSettings;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Sabberworm\CSS\Parsing\Anchor
|
||||
*/
|
||||
public function anchor()
|
||||
{
|
||||
return new Anchor($this->iCurrentPosition, $this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $iPosition
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setPosition($iPosition)
|
||||
{
|
||||
$this->iCurrentPosition = $iPosition;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $bIgnoreCase
|
||||
*
|
||||
* @return string
|
||||
* @return non-empty-string
|
||||
*
|
||||
* @throws UnexpectedTokenException
|
||||
*
|
||||
* @internal since V8.8.0
|
||||
*/
|
||||
public function parseIdentifier($bIgnoreCase = true)
|
||||
public function parseIdentifier(bool $ignoreCase = true): string
|
||||
{
|
||||
if ($this->isEnd()) {
|
||||
throw new UnexpectedEOFException('', '', 'identifier', $this->iLineNo);
|
||||
throw new UnexpectedEOFException('', '', 'identifier', $this->lineNumber);
|
||||
}
|
||||
$sResult = $this->parseCharacter(true);
|
||||
if ($sResult === null) {
|
||||
throw new UnexpectedTokenException($sResult, $this->peek(5), 'identifier', $this->iLineNo);
|
||||
$result = $this->parseCharacter(true);
|
||||
if ($result === null) {
|
||||
throw new UnexpectedTokenException('', $this->peek(5), 'identifier', $this->lineNumber);
|
||||
}
|
||||
$sCharacter = null;
|
||||
while (!$this->isEnd() && ($sCharacter = $this->parseCharacter(true)) !== null) {
|
||||
if (preg_match('/[a-zA-Z0-9\x{00A0}-\x{FFFF}_-]/Sux', $sCharacter)) {
|
||||
$sResult .= $sCharacter;
|
||||
while (!$this->isEnd() && ($character = $this->parseCharacter(true)) !== null) {
|
||||
if (preg_match('/[a-zA-Z0-9\\x{00A0}-\\x{FFFF}_-]/Sux', $character) !== 0) {
|
||||
$result .= $character;
|
||||
} else {
|
||||
$sResult .= '\\' . $sCharacter;
|
||||
$result .= '\\' . $character;
|
||||
}
|
||||
}
|
||||
if ($bIgnoreCase) {
|
||||
$sResult = $this->strtolower($sResult);
|
||||
if ($ignoreCase) {
|
||||
$result = $this->strtolower($result);
|
||||
}
|
||||
return $sResult;
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $bIsForIdentifier
|
||||
*
|
||||
* @return string|null
|
||||
*
|
||||
* @throws UnexpectedEOFException
|
||||
* @throws UnexpectedTokenException
|
||||
*
|
||||
* @internal since V8.8.0
|
||||
*/
|
||||
public function parseCharacter($bIsForIdentifier)
|
||||
public function parseCharacter(bool $isForIdentifier): ?string
|
||||
{
|
||||
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')) {
|
||||
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) {
|
||||
$hexCodePoint = $this->consumeExpression('/^[0-9a-fA-F]{1,6}/u', 6);
|
||||
if ($this->strlen($hexCodePoint) < 6) {
|
||||
// Consume whitespace after incomplete unicode escape
|
||||
if (preg_match('/\\s/isSu', $this->peek())) {
|
||||
if ($this->comes('\r\n')) {
|
||||
if (preg_match('/\\s/isSu', $this->peek()) !== 0) {
|
||||
if ($this->comes('\\r\\n')) {
|
||||
$this->consume(2);
|
||||
} else {
|
||||
$this->consume(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
$iUnicode = intval($sUnicode, 16);
|
||||
$sUtf32 = "";
|
||||
$codePoint = \intval($hexCodePoint, 16);
|
||||
$utf32EncodedCharacter = '';
|
||||
for ($i = 0; $i < 4; ++$i) {
|
||||
$sUtf32 .= chr($iUnicode & 0xff);
|
||||
$iUnicode = $iUnicode >> 8;
|
||||
$utf32EncodedCharacter .= \chr($codePoint & 0xff);
|
||||
$codePoint = $codePoint >> 8;
|
||||
}
|
||||
return iconv('utf-32le', $this->sCharset, $sUtf32);
|
||||
return iconv('utf-32le', $this->charset, $utf32EncodedCharacter);
|
||||
}
|
||||
if ($bIsForIdentifier) {
|
||||
$peek = ord($this->peek());
|
||||
if ($isForIdentifier) {
|
||||
$peek = \ord($this->peek());
|
||||
// Ranges: a-z A-Z 0-9 - _
|
||||
if (
|
||||
($peek >= 97 && $peek <= 122)
|
||||
@@ -229,116 +182,147 @@ class ParserState
|
||||
} else {
|
||||
return $this->consume(1);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, Comment>|void
|
||||
* Consumes whitespace and/or comments until the next non-whitespace character that isn't a slash opening a comment.
|
||||
*
|
||||
* @param list<Comment> $comments Any comments consumed will be appended to this array.
|
||||
*
|
||||
* @return string the whitespace consumed, without the comments
|
||||
*
|
||||
* @throws UnexpectedEOFException
|
||||
* @throws UnexpectedTokenException
|
||||
*
|
||||
* @phpstan-impure
|
||||
* This method may change the state of the object by advancing the internal position;
|
||||
* it does not simply 'get' a value.
|
||||
*/
|
||||
public function consumeWhiteSpace()
|
||||
public function consumeWhiteSpace(array &$comments = []): string
|
||||
{
|
||||
$aComments = [];
|
||||
$consumed = '';
|
||||
do {
|
||||
while (preg_match('/\\s/isSu', $this->peek()) === 1) {
|
||||
$this->consume(1);
|
||||
$consumed .= $this->consume(1);
|
||||
}
|
||||
if ($this->oParserSettings->bLenientParsing) {
|
||||
if ($this->parserSettings->usesLenientParsing()) {
|
||||
try {
|
||||
$oComment = $this->consumeComment();
|
||||
$comment = $this->consumeComment();
|
||||
} catch (UnexpectedEOFException $e) {
|
||||
$this->iCurrentPosition = $this->iLength;
|
||||
return $aComments;
|
||||
$this->currentPosition = \count($this->characters);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
$oComment = $this->consumeComment();
|
||||
$comment = $this->consumeComment();
|
||||
}
|
||||
if ($oComment !== false) {
|
||||
$aComments[] = $oComment;
|
||||
if ($comment instanceof Comment) {
|
||||
$comments[] = $comment;
|
||||
}
|
||||
} while ($oComment !== false);
|
||||
return $aComments;
|
||||
} while ($comment instanceof Comment);
|
||||
|
||||
return $consumed;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $sString
|
||||
* @param bool $bCaseInsensitive
|
||||
*
|
||||
* @return bool
|
||||
* @param non-empty-string $string
|
||||
*/
|
||||
public function comes($sString, $bCaseInsensitive = false)
|
||||
public function comes(string $string, bool $caseInsensitive = false): bool
|
||||
{
|
||||
$sPeek = $this->peek(strlen($sString));
|
||||
return ($sPeek == '')
|
||||
? false
|
||||
: $this->streql($sPeek, $sString, $bCaseInsensitive);
|
||||
$peek = $this->peek(\strlen($string));
|
||||
|
||||
return ($peek !== '') && $this->streql($peek, $string, $caseInsensitive);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $iLength
|
||||
* @param int $iOffset
|
||||
*
|
||||
* @return string
|
||||
* @param int<1, max> $length
|
||||
* @param int<0, max> $offset
|
||||
*/
|
||||
public function peek($iLength = 1, $iOffset = 0)
|
||||
public function peek(int $length = 1, int $offset = 0): string
|
||||
{
|
||||
$iOffset += $this->iCurrentPosition;
|
||||
if ($iOffset >= $this->iLength) {
|
||||
$offset += $this->currentPosition;
|
||||
if ($offset >= \count($this->characters)) {
|
||||
return '';
|
||||
}
|
||||
return $this->substr($iOffset, $iLength);
|
||||
|
||||
return $this->substr($offset, $length);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $mValue
|
||||
*
|
||||
* @return string
|
||||
* @param string|int<1, max> $value
|
||||
*
|
||||
* @throws UnexpectedEOFException
|
||||
* @throws UnexpectedTokenException
|
||||
*/
|
||||
public function consume($mValue = 1)
|
||||
public function consume($value = 1): string
|
||||
{
|
||||
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);
|
||||
if (\is_string($value)) {
|
||||
$numberOfLines = \substr_count($value, "\n");
|
||||
$length = $this->strlen($value);
|
||||
if (!$this->streql($this->substr($this->currentPosition, $length), $value)) {
|
||||
throw new UnexpectedTokenException(
|
||||
$value,
|
||||
$this->peek(\max($length, 5)),
|
||||
'literal',
|
||||
$this->lineNumber
|
||||
);
|
||||
}
|
||||
$this->iLineNo += $iLineCount;
|
||||
$this->iCurrentPosition += $this->strlen($mValue);
|
||||
return $mValue;
|
||||
|
||||
$this->lineNumber += $numberOfLines;
|
||||
$this->currentPosition += $this->strlen($value);
|
||||
$result = $value;
|
||||
} else {
|
||||
if ($this->iCurrentPosition + $mValue > $this->iLength) {
|
||||
throw new UnexpectedEOFException($mValue, $this->peek(5), 'count', $this->iLineNo);
|
||||
if ($this->currentPosition + $value > \count($this->characters)) {
|
||||
throw new UnexpectedEOFException((string) $value, $this->peek(5), 'count', $this->lineNumber);
|
||||
}
|
||||
$sResult = $this->substr($this->iCurrentPosition, $mValue);
|
||||
$iLineCount = substr_count($sResult, "\n");
|
||||
$this->iLineNo += $iLineCount;
|
||||
$this->iCurrentPosition += $mValue;
|
||||
return $sResult;
|
||||
|
||||
$result = $this->substr($this->currentPosition, $value);
|
||||
$numberOfLines = \substr_count($result, "\n");
|
||||
$this->lineNumber += $numberOfLines;
|
||||
$this->currentPosition += $value;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $mExpression
|
||||
* @param int|null $iMaxLength
|
||||
* If the possibly-expected next content is next, consume it.
|
||||
*
|
||||
* @return string
|
||||
* @param non-empty-string $nextContent
|
||||
*
|
||||
* @return bool whether the possibly-expected content was found and consumed
|
||||
*/
|
||||
public function consumeIfComes(string $nextContent): bool
|
||||
{
|
||||
$length = $this->strlen($nextContent);
|
||||
if (!$this->streql($this->substr($this->currentPosition, $length), $nextContent)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$numberOfLines = \substr_count($nextContent, "\n");
|
||||
$this->lineNumber += $numberOfLines;
|
||||
$this->currentPosition += $this->strlen($nextContent);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $expression
|
||||
* @param int<1, max>|null $maximumLength
|
||||
*
|
||||
* @throws UnexpectedEOFException
|
||||
* @throws UnexpectedTokenException
|
||||
*/
|
||||
public function consumeExpression($mExpression, $iMaxLength = null)
|
||||
public function consumeExpression(string $expression, ?int $maximumLength = null): string
|
||||
{
|
||||
$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]);
|
||||
$matches = null;
|
||||
$input = ($maximumLength !== null) ? $this->peek($maximumLength) : $this->inputLeft();
|
||||
if (preg_match($expression, $input, $matches, PREG_OFFSET_CAPTURE) !== 1) {
|
||||
throw new UnexpectedTokenException($expression, $this->peek(5), 'expression', $this->lineNumber);
|
||||
}
|
||||
throw new UnexpectedTokenException($mExpression, $this->peek(5), 'expression', $this->iLineNo);
|
||||
|
||||
return $this->consume($matches[0][0]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -346,13 +330,14 @@ class ParserState
|
||||
*/
|
||||
public function consumeComment()
|
||||
{
|
||||
$mComment = false;
|
||||
$lineNumber = $this->lineNumber;
|
||||
$comment = null;
|
||||
|
||||
if ($this->comes('/*')) {
|
||||
$iLineNo = $this->iLineNo;
|
||||
$this->consume(1);
|
||||
$mComment = '';
|
||||
$comment = '';
|
||||
while (($char = $this->consume(1)) !== '') {
|
||||
$mComment .= $char;
|
||||
$comment .= $char;
|
||||
if ($this->comes('*/')) {
|
||||
$this->consume(2);
|
||||
break;
|
||||
@@ -360,193 +345,157 @@ class ParserState
|
||||
}
|
||||
}
|
||||
|
||||
if ($mComment !== false) {
|
||||
// We skip the * which was included in the comment.
|
||||
return new Comment(substr($mComment, 1), $iLineNo);
|
||||
}
|
||||
|
||||
return $mComment;
|
||||
// We skip the * which was included in the comment.
|
||||
return \is_string($comment) ? new Comment(\substr($comment, 1), $lineNumber) : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isEnd()
|
||||
public function isEnd(): bool
|
||||
{
|
||||
return $this->iCurrentPosition >= $this->iLength;
|
||||
return $this->currentPosition >= \count($this->characters);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<array-key, string>|string $aEnd
|
||||
* @param string $bIncludeEnd
|
||||
* @param string $consumeEnd
|
||||
* @param array<int, Comment> $comments
|
||||
*
|
||||
* @return string
|
||||
* @param list<string|self::EOF>|string|self::EOF $stopCharacters
|
||||
* @param list<Comment> $comments
|
||||
*
|
||||
* @throws UnexpectedEOFException
|
||||
* @throws UnexpectedTokenException
|
||||
*/
|
||||
public function consumeUntil($aEnd, $bIncludeEnd = false, $consumeEnd = false, array &$comments = [])
|
||||
{
|
||||
$aEnd = is_array($aEnd) ? $aEnd : [$aEnd];
|
||||
$out = '';
|
||||
$start = $this->iCurrentPosition;
|
||||
public function consumeUntil(
|
||||
$stopCharacters,
|
||||
bool $includeEnd = false,
|
||||
bool $consumeEnd = false,
|
||||
array &$comments = []
|
||||
): string {
|
||||
$stopCharacters = \is_array($stopCharacters) ? $stopCharacters : [$stopCharacters];
|
||||
$consumedCharacters = '';
|
||||
$start = $this->currentPosition;
|
||||
|
||||
$comments = \array_merge($comments, $this->consumeComments());
|
||||
while (!$this->isEnd()) {
|
||||
$char = $this->consume(1);
|
||||
if (in_array($char, $aEnd)) {
|
||||
if ($bIncludeEnd) {
|
||||
$out .= $char;
|
||||
$character = $this->consume(1);
|
||||
if (\in_array($character, $stopCharacters, true)) {
|
||||
if ($includeEnd) {
|
||||
$consumedCharacters .= $character;
|
||||
} elseif (!$consumeEnd) {
|
||||
$this->iCurrentPosition -= $this->strlen($char);
|
||||
$this->currentPosition -= $this->strlen($character);
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
$out .= $char;
|
||||
if ($comment = $this->consumeComment()) {
|
||||
$comments[] = $comment;
|
||||
return $consumedCharacters;
|
||||
}
|
||||
$consumedCharacters .= $character;
|
||||
$comments = \array_merge($comments, $this->consumeComments());
|
||||
}
|
||||
|
||||
if (in_array(self::EOF, $aEnd)) {
|
||||
return $out;
|
||||
if (\in_array(self::EOF, $stopCharacters, true)) {
|
||||
return $consumedCharacters;
|
||||
}
|
||||
|
||||
$this->iCurrentPosition = $start;
|
||||
$this->currentPosition = $start;
|
||||
throw new UnexpectedEOFException(
|
||||
'One of ("' . implode('","', $aEnd) . '")',
|
||||
'One of ("' . \implode('","', $stopCharacters) . '")',
|
||||
$this->peek(5),
|
||||
'search',
|
||||
$this->iLineNo
|
||||
$this->lineNumber
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
private function inputLeft()
|
||||
private function inputLeft(): string
|
||||
{
|
||||
return $this->substr($this->iCurrentPosition, -1);
|
||||
return $this->substr($this->currentPosition, -1);
|
||||
}
|
||||
|
||||
public function streql(string $string1, string $string2, bool $caseInsensitive = true): bool
|
||||
{
|
||||
return $caseInsensitive
|
||||
? ($this->strtolower($string1) === $this->strtolower($string2))
|
||||
: ($string1 === $string2);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $sString1
|
||||
* @param string $sString2
|
||||
* @param bool $bCaseInsensitive
|
||||
*
|
||||
* @return bool
|
||||
* @param int<1, max> $numberOfCharacters
|
||||
*/
|
||||
public function streql($sString1, $sString2, $bCaseInsensitive = true)
|
||||
public function backtrack(int $numberOfCharacters): void
|
||||
{
|
||||
if ($bCaseInsensitive) {
|
||||
return $this->strtolower($sString1) === $this->strtolower($sString2);
|
||||
} else {
|
||||
return $sString1 === $sString2;
|
||||
}
|
||||
$this->currentPosition -= $numberOfCharacters;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $iAmount
|
||||
*
|
||||
* @return void
|
||||
* @return int<0, max>
|
||||
*/
|
||||
public function backtrack($iAmount)
|
||||
public function strlen(string $string): int
|
||||
{
|
||||
$this->iCurrentPosition -= $iAmount;
|
||||
return $this->parserSettings->hasMultibyteSupport()
|
||||
? \mb_strlen($string, $this->charset)
|
||||
: \strlen($string);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $sString
|
||||
*
|
||||
* @return int
|
||||
* @param int<0, max> $offset
|
||||
*/
|
||||
public function strlen($sString)
|
||||
private function substr(int $offset, int $length): string
|
||||
{
|
||||
if ($this->oParserSettings->bMultibyteSupport) {
|
||||
return mb_strlen($sString, $this->sCharset);
|
||||
} else {
|
||||
return strlen($sString);
|
||||
if ($length < 0) {
|
||||
$length = \count($this->characters) - $offset + $length;
|
||||
}
|
||||
if ($offset + $length > \count($this->characters)) {
|
||||
$length = \count($this->characters) - $offset;
|
||||
}
|
||||
$result = '';
|
||||
while ($length > 0) {
|
||||
$result .= $this->characters[$offset];
|
||||
$offset++;
|
||||
$length--;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $iStart
|
||||
* @param int $iLength
|
||||
*
|
||||
* @return string
|
||||
* @return ($string is non-empty-string ? non-empty-string : string)
|
||||
*/
|
||||
private function substr($iStart, $iLength)
|
||||
private function strtolower(string $string): string
|
||||
{
|
||||
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;
|
||||
return $this->parserSettings->hasMultibyteSupport()
|
||||
? \mb_strtolower($string, $this->charset)
|
||||
: \strtolower($string);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $sString
|
||||
*
|
||||
* @return string
|
||||
* @return list<string>
|
||||
*/
|
||||
private function strtolower($sString)
|
||||
private function strsplit(string $string): array
|
||||
{
|
||||
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);
|
||||
if ($this->parserSettings->hasMultibyteSupport()) {
|
||||
if ($this->streql($this->charset, 'utf-8')) {
|
||||
$result = preg_split('//u', $string, -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);
|
||||
$length = \mb_strlen($string, $this->charset);
|
||||
$result = [];
|
||||
for ($i = 0; $i < $length; ++$i) {
|
||||
$result[] = \mb_substr($string, $i, 1, $this->charset);
|
||||
}
|
||||
return $aResult;
|
||||
}
|
||||
} else {
|
||||
if ($sString === '') {
|
||||
return [];
|
||||
} else {
|
||||
return str_split($sString);
|
||||
}
|
||||
$result = ($string !== '') ? \str_split($string) : [];
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $sString
|
||||
* @param string $sNeedle
|
||||
* @param int $iOffset
|
||||
*
|
||||
* @return int|false
|
||||
* @return list<Comment>
|
||||
*/
|
||||
private function strpos($sString, $sNeedle, $iOffset)
|
||||
private function consumeComments(): array
|
||||
{
|
||||
if ($this->oParserSettings->bMultibyteSupport) {
|
||||
return mb_strpos($sString, $sNeedle, $iOffset, $this->sCharset);
|
||||
} else {
|
||||
return strpos($sString, $sNeedle, $iOffset);
|
||||
$comments = [];
|
||||
|
||||
while (true) {
|
||||
$comment = $this->consumeComment();
|
||||
if ($comment instanceof Comment) {
|
||||
$comments[] = $comment;
|
||||
} else {
|
||||
return $comments;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sabberworm\CSS\Parsing;
|
||||
|
||||
use Sabberworm\CSS\Position\Position;
|
||||
@@ -10,15 +12,14 @@ class SourceException extends \Exception implements Positionable
|
||||
use Position;
|
||||
|
||||
/**
|
||||
* @param string $sMessage
|
||||
* @param int $iLineNo
|
||||
* @param int<1, max>|null $lineNumber
|
||||
*/
|
||||
public function __construct($sMessage, $iLineNo = 0)
|
||||
public function __construct(string $message, ?int $lineNumber = null)
|
||||
{
|
||||
$this->setPosition($iLineNo);
|
||||
if (!empty($iLineNo)) {
|
||||
$sMessage .= " [line no: $iLineNo]";
|
||||
$this->setPosition($lineNumber);
|
||||
if ($lineNumber !== null) {
|
||||
$message .= " [line no: $lineNumber]";
|
||||
}
|
||||
parent::__construct($sMessage);
|
||||
parent::__construct($message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sabberworm\CSS\Parsing;
|
||||
|
||||
/**
|
||||
@@ -7,6 +9,4 @@ namespace Sabberworm\CSS\Parsing;
|
||||
*
|
||||
* Extends `UnexpectedTokenException` in order to preserve backwards compatibility.
|
||||
*/
|
||||
class UnexpectedEOFException extends UnexpectedTokenException
|
||||
{
|
||||
}
|
||||
final class UnexpectedEOFException extends UnexpectedTokenException {}
|
||||
|
||||
+15
-35
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sabberworm\CSS\Parsing;
|
||||
|
||||
/**
|
||||
@@ -8,44 +10,22 @@ namespace Sabberworm\CSS\Parsing;
|
||||
class UnexpectedTokenException extends SourceException
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
* @param 'literal'|'identifier'|'count'|'expression'|'search'|'custom' $matchType
|
||||
* @param int<1, max>|null $lineNumber
|
||||
*/
|
||||
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)
|
||||
public function __construct(string $expected, string $found, string $matchType = 'literal', ?int $lineNumber = null)
|
||||
{
|
||||
$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");
|
||||
$message = "Token “{$expected}” ({$matchType}) not found. Got “{$found}”.";
|
||||
if ($matchType === 'search') {
|
||||
$message = "Search for “{$expected}” returned no results. Context: “{$found}”.";
|
||||
} elseif ($matchType === 'count') {
|
||||
$message = "Next token was expected to have {$expected} chars. Context: “{$found}”.";
|
||||
} elseif ($matchType === 'identifier') {
|
||||
$message = "Identifier expected. Got “{$found}”";
|
||||
} elseif ($matchType === 'custom') {
|
||||
$message = \trim("$expected $found");
|
||||
}
|
||||
|
||||
parent::__construct($sMessage, $iLineNo);
|
||||
parent::__construct($message, $lineNumber);
|
||||
}
|
||||
}
|
||||
|
||||
+9
-26
@@ -26,47 +26,30 @@ trait Position
|
||||
/**
|
||||
* @return int<1, max>|null
|
||||
*/
|
||||
public function getLineNumber()
|
||||
public function getLineNumber(): ?int
|
||||
{
|
||||
return $this->lineNumber;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int<0, max>
|
||||
*/
|
||||
public function getLineNo()
|
||||
{
|
||||
$lineNumber = $this->getLineNumber();
|
||||
|
||||
return $lineNumber !== null ? $lineNumber : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int<0, max>|null
|
||||
*/
|
||||
public function getColumnNumber()
|
||||
public function getColumnNumber(): ?int
|
||||
{
|
||||
return $this->columnNumber;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int<0, max>
|
||||
*/
|
||||
public function getColNo()
|
||||
{
|
||||
$columnNumber = $this->getColumnNumber();
|
||||
|
||||
return $columnNumber !== null ? $columnNumber : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int<0, max>|null $lineNumber
|
||||
* @param int<1, max>|null $lineNumber
|
||||
* @param int<0, max>|null $columnNumber
|
||||
*
|
||||
* @return $this fluent interface
|
||||
*/
|
||||
public function setPosition($lineNumber, $columnNumber = null)
|
||||
public function setPosition(?int $lineNumber, ?int $columnNumber = null): Positionable
|
||||
{
|
||||
// The conditional is for backwards compatibility (backcompat); `0` will not be allowed in future.
|
||||
$this->lineNumber = $lineNumber !== 0 ? $lineNumber : null;
|
||||
$this->lineNumber = $lineNumber;
|
||||
$this->columnNumber = $columnNumber;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,32 +14,18 @@ interface Positionable
|
||||
/**
|
||||
* @return int<1, max>|null
|
||||
*/
|
||||
public function getLineNumber();
|
||||
|
||||
/**
|
||||
* @return int<0, max>
|
||||
*
|
||||
* @deprecated in version 8.9.0, will be removed in v9.0. Use `getLineNumber()` instead.
|
||||
*/
|
||||
public function getLineNo();
|
||||
public function getLineNumber(): ?int;
|
||||
|
||||
/**
|
||||
* @return int<0, max>|null
|
||||
*/
|
||||
public function getColumnNumber();
|
||||
public function getColumnNumber(): ?int;
|
||||
|
||||
/**
|
||||
* @return int<0, max>
|
||||
*
|
||||
* @deprecated in version 8.9.0, will be removed in v9.0. Use `getColumnNumber()` instead.
|
||||
*/
|
||||
public function getColNo();
|
||||
|
||||
/**
|
||||
* @param int<0, max>|null $lineNumber
|
||||
* Providing zero for this parameter is deprecated in version 8.9.0, and will not be supported from v9.0.
|
||||
* Use `null` instead when no line number is available.
|
||||
* @param int<1, max>|null $lineNumber
|
||||
* @param int<0, max>|null $columnNumber
|
||||
*
|
||||
* @return $this fluent interface
|
||||
*/
|
||||
public function setPosition($lineNumber, $columnNumber = null);
|
||||
public function setPosition(?int $lineNumber, ?int $columnNumber = null): Positionable;
|
||||
}
|
||||
|
||||
+21
-22
@@ -1,38 +1,37 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sabberworm\CSS\Property;
|
||||
|
||||
use Sabberworm\CSS\Comment\Commentable;
|
||||
use Sabberworm\CSS\Renderable;
|
||||
use Sabberworm\CSS\CSSList\CSSListItem;
|
||||
|
||||
interface AtRule extends Renderable, Commentable
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @var string
|
||||
*
|
||||
* @internal since 8.5.2
|
||||
*/
|
||||
const BLOCK_RULES = 'media/document/supports/region-style/font-feature-values';
|
||||
public const BLOCK_RULES = [
|
||||
'media',
|
||||
'document',
|
||||
'supports',
|
||||
'region-style',
|
||||
'font-feature-values',
|
||||
'container',
|
||||
'layer',
|
||||
'scope',
|
||||
'starting-style',
|
||||
];
|
||||
|
||||
/**
|
||||
* … and more font-specific ones (to be used inside font-feature-values)
|
||||
*
|
||||
* @var string
|
||||
*
|
||||
* @internal since 8.5.2
|
||||
* @return non-empty-string
|
||||
*/
|
||||
const SET_RULES = 'font-face/counter-style/page/swash/styleset/annotation';
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function atRuleName();
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function atRuleArgs();
|
||||
public function atRuleName(): string;
|
||||
}
|
||||
|
||||
+48
-90
@@ -1,156 +1,114 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sabberworm\CSS\Property;
|
||||
|
||||
use Sabberworm\CSS\Comment\Comment;
|
||||
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 string
|
||||
* @var CSSString|URL
|
||||
*/
|
||||
private $mUrl;
|
||||
private $url;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
* @var string|null
|
||||
*/
|
||||
private $sPrefix;
|
||||
private $prefix;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
* @param CSSString|URL $url
|
||||
* @param int<1, max>|null $lineNumber
|
||||
*/
|
||||
private $iLineNo;
|
||||
|
||||
/**
|
||||
* @var array<array-key, Comment>
|
||||
*
|
||||
* @internal since 8.8.0
|
||||
*/
|
||||
protected $aComments;
|
||||
|
||||
/**
|
||||
* @param string $mUrl
|
||||
* @param string|null $sPrefix
|
||||
* @param int $iLineNo
|
||||
*/
|
||||
public function __construct($mUrl, $sPrefix = null, $iLineNo = 0)
|
||||
public function __construct($url, ?string $prefix = null, ?int $lineNumber = null)
|
||||
{
|
||||
$this->mUrl = $mUrl;
|
||||
$this->sPrefix = $sPrefix;
|
||||
$this->setPosition($iLineNo);
|
||||
$this->aComments = [];
|
||||
$this->url = $url;
|
||||
$this->prefix = $prefix;
|
||||
$this->setPosition($lineNumber);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*
|
||||
* @deprecated in V8.8.0, will be removed in V9.0.0. Use `render` instead.
|
||||
* @return non-empty-string
|
||||
*/
|
||||
public function __toString()
|
||||
public function render(OutputFormat $outputFormat): string
|
||||
{
|
||||
return $this->render(new OutputFormat());
|
||||
return '@namespace ' . ($this->prefix === null ? '' : $this->prefix . ' ')
|
||||
. $this->url->render($outputFormat) . ';';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param OutputFormat|null $oOutputFormat
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function render($oOutputFormat)
|
||||
{
|
||||
return '@namespace ' . ($this->sPrefix === null ? '' : $this->sPrefix . ' ')
|
||||
. $this->mUrl->render($oOutputFormat) . ';';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
* @return CSSString|URL
|
||||
*/
|
||||
public function getUrl()
|
||||
{
|
||||
return $this->mUrl;
|
||||
return $this->url;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getPrefix()
|
||||
public function getPrefix(): ?string
|
||||
{
|
||||
return $this->sPrefix;
|
||||
return $this->prefix;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $mUrl
|
||||
*
|
||||
* @return void
|
||||
* @param CSSString|URL $url
|
||||
*/
|
||||
public function setUrl($mUrl)
|
||||
public function setUrl($url): void
|
||||
{
|
||||
$this->mUrl = $mUrl;
|
||||
$this->url = $url;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $sPrefix
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setPrefix($sPrefix)
|
||||
public function setPrefix(string $prefix): void
|
||||
{
|
||||
$this->sPrefix = $sPrefix;
|
||||
$this->prefix = $prefix;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
* @return non-empty-string
|
||||
*/
|
||||
public function atRuleName()
|
||||
public function atRuleName(): string
|
||||
{
|
||||
return 'namespace';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, string>
|
||||
* @return array{0: CSSString|URL|non-empty-string, 1?: CSSString|URL}
|
||||
*/
|
||||
public function atRuleArgs()
|
||||
public function atRuleArgs(): array
|
||||
{
|
||||
$aResult = [$this->mUrl];
|
||||
if ($this->sPrefix) {
|
||||
array_unshift($aResult, $this->sPrefix);
|
||||
$result = [$this->url];
|
||||
if (\is_string($this->prefix) && $this->prefix !== '') {
|
||||
\array_unshift($result, $this->prefix);
|
||||
}
|
||||
return $aResult;
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<array-key, Comment> $aComments
|
||||
* @return array<string, bool|int|float|string|array<mixed>|null>
|
||||
*
|
||||
* @return void
|
||||
* @internal
|
||||
*/
|
||||
public function addComments(array $aComments)
|
||||
public function getArrayRepresentation(): array
|
||||
{
|
||||
$this->aComments = array_merge($this->aComments, $aComments);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<array-key, Comment>
|
||||
*/
|
||||
public function getComments()
|
||||
{
|
||||
return $this->aComments;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<array-key, Comment> $aComments
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setComments(array $aComments)
|
||||
{
|
||||
$this->aComments = $aComments;
|
||||
return [
|
||||
'class' => $this->getShortClassName(),
|
||||
// We're using `uri` here instead of `url` to better match the spec.
|
||||
'uri' => $this->url->getArrayRepresentation(),
|
||||
'prefix' => $this->prefix,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
+34
-80
@@ -1,11 +1,14 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sabberworm\CSS\Property;
|
||||
|
||||
use Sabberworm\CSS\Comment\Comment;
|
||||
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;
|
||||
|
||||
/**
|
||||
@@ -18,118 +21,69 @@ use Sabberworm\CSS\Value\CSSString;
|
||||
*/
|
||||
class Charset implements AtRule, Positionable
|
||||
{
|
||||
use CommentContainer;
|
||||
use Position;
|
||||
use ShortClassNameProvider;
|
||||
|
||||
/**
|
||||
* @var CSSString
|
||||
*/
|
||||
private $oCharset;
|
||||
private $charset;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*
|
||||
* @internal since 8.8.0
|
||||
* @param int<1, max>|null $lineNumber
|
||||
*/
|
||||
protected $iLineNo;
|
||||
|
||||
/**
|
||||
* @var array<array-key, Comment>
|
||||
*
|
||||
* @internal since 8.8.0
|
||||
*/
|
||||
protected $aComments;
|
||||
|
||||
/**
|
||||
* @param CSSString $oCharset
|
||||
* @param int $iLineNo
|
||||
*/
|
||||
public function __construct(CSSString $oCharset, $iLineNo = 0)
|
||||
public function __construct(CSSString $charset, ?int $lineNumber = null)
|
||||
{
|
||||
$this->oCharset = $oCharset;
|
||||
$this->setPosition($iLineNo);
|
||||
$this->aComments = [];
|
||||
$this->charset = $charset;
|
||||
$this->setPosition($lineNumber);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|CSSString $oCharset
|
||||
*
|
||||
* @return void
|
||||
* @param string|CSSString $charset
|
||||
*/
|
||||
public function setCharset($sCharset)
|
||||
public function setCharset($charset): void
|
||||
{
|
||||
$sCharset = $sCharset instanceof CSSString ? $sCharset : new CSSString($sCharset);
|
||||
$this->oCharset = $sCharset;
|
||||
$charset = $charset instanceof CSSString ? $charset : new CSSString($charset);
|
||||
$this->charset = $charset;
|
||||
}
|
||||
|
||||
public function getCharset(): string
|
||||
{
|
||||
return $this->charset->getString();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
* @return non-empty-string
|
||||
*/
|
||||
public function getCharset()
|
||||
public function render(OutputFormat $outputFormat): string
|
||||
{
|
||||
return $this->oCharset->getString();
|
||||
return "{$outputFormat->getFormatter()->comments($this)}@charset {$this->charset->render($outputFormat)};";
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*
|
||||
* @deprecated in V8.8.0, will be removed in V9.0.0. Use `render` instead.
|
||||
* @return non-empty-string
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
return $this->render(new OutputFormat());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param OutputFormat|null $oOutputFormat
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function render($oOutputFormat)
|
||||
{
|
||||
return "{$oOutputFormat->comments($this)}@charset {$this->oCharset->render($oOutputFormat)};";
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function atRuleName()
|
||||
public function atRuleName(): string
|
||||
{
|
||||
return 'charset';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function atRuleArgs()
|
||||
public function atRuleArgs(): CSSString
|
||||
{
|
||||
return $this->oCharset;
|
||||
return $this->charset;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<array-key, Comment> $aComments
|
||||
* @return array<string, bool|int|float|string|array<mixed>|null>
|
||||
*
|
||||
* @return void
|
||||
* @internal
|
||||
*/
|
||||
public function addComments(array $aComments)
|
||||
public function getArrayRepresentation(): array
|
||||
{
|
||||
$this->aComments = array_merge($this->aComments, $aComments);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<array-key, Comment>
|
||||
*/
|
||||
public function getComments()
|
||||
{
|
||||
return $this->aComments;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<array-key, Comment> $aComments
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setComments(array $aComments)
|
||||
{
|
||||
$this->aComments = $aComments;
|
||||
return [
|
||||
'class' => $this->getShortClassName(),
|
||||
'charset' => $this->charset->getArrayRepresentation(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,240 @@
|
||||
<?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\ShortClassNameProvider;
|
||||
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;
|
||||
use ShortClassNameProvider;
|
||||
|
||||
/**
|
||||
* @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
|
||||
{
|
||||
return [
|
||||
'class' => $this->getShortClassName(),
|
||||
'propertyName' => $this->propertyName,
|
||||
// We're using the term "property value" here to match the wording used in the specs:
|
||||
// https://www.w3.org/TR/CSS22/syndata.html#declaration
|
||||
'propertyValue' => $this->value,
|
||||
'important' => $this->isImportant,
|
||||
];
|
||||
}
|
||||
}
|
||||
+51
-91
@@ -1,11 +1,14 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sabberworm\CSS\Property;
|
||||
|
||||
use Sabberworm\CSS\Comment\Comment;
|
||||
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;
|
||||
|
||||
/**
|
||||
@@ -13,130 +16,87 @@ use Sabberworm\CSS\Value\URL;
|
||||
*/
|
||||
class Import implements AtRule, Positionable
|
||||
{
|
||||
use CommentContainer;
|
||||
use Position;
|
||||
use ShortClassNameProvider;
|
||||
|
||||
/**
|
||||
* @var URL
|
||||
*/
|
||||
private $oLocation;
|
||||
private $location;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
* @var string|null
|
||||
*/
|
||||
private $sMediaQuery;
|
||||
private $mediaQuery;
|
||||
|
||||
/**
|
||||
* @var array<array-key, Comment>
|
||||
*
|
||||
* @internal since 8.8.0
|
||||
* @param int<1, max>|null $lineNumber
|
||||
*/
|
||||
protected $aComments;
|
||||
|
||||
/**
|
||||
* @param URL $oLocation
|
||||
* @param string $sMediaQuery
|
||||
* @param int $iLineNo
|
||||
*/
|
||||
public function __construct(URL $oLocation, $sMediaQuery, $iLineNo = 0)
|
||||
public function __construct(URL $location, ?string $mediaQuery, ?int $lineNumber = null)
|
||||
{
|
||||
$this->oLocation = $oLocation;
|
||||
$this->sMediaQuery = $sMediaQuery;
|
||||
$this->setPosition($iLineNo);
|
||||
$this->aComments = [];
|
||||
$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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param URL $oLocation
|
||||
*
|
||||
* @return void
|
||||
* @return non-empty-string
|
||||
*/
|
||||
public function setLocation($oLocation)
|
||||
public function render(OutputFormat $outputFormat): string
|
||||
{
|
||||
$this->oLocation = $oLocation;
|
||||
return $outputFormat->getFormatter()->comments($this) . '@import ' . $this->location->render($outputFormat)
|
||||
. ($this->mediaQuery === null ? '' : ' ' . $this->mediaQuery) . ';';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return URL
|
||||
* @return non-empty-string
|
||||
*/
|
||||
public function getLocation()
|
||||
{
|
||||
return $this->oLocation;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*
|
||||
* @deprecated in V8.8.0, will be removed in V9.0.0. Use `render` instead.
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
return $this->render(new OutputFormat());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param OutputFormat|null $oOutputFormat
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function render($oOutputFormat)
|
||||
{
|
||||
return $oOutputFormat->comments($this) . "@import " . $this->oLocation->render($oOutputFormat)
|
||||
. ($this->sMediaQuery === null ? '' : ' ' . $this->sMediaQuery) . ';';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function atRuleName()
|
||||
public function atRuleName(): string
|
||||
{
|
||||
return 'import';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, URL|string>
|
||||
* @return array{0: URL, 1?: non-empty-string}
|
||||
*/
|
||||
public function atRuleArgs()
|
||||
public function atRuleArgs(): array
|
||||
{
|
||||
$aResult = [$this->oLocation];
|
||||
if ($this->sMediaQuery) {
|
||||
array_push($aResult, $this->sMediaQuery);
|
||||
$result = [$this->location];
|
||||
if (\is_string($this->mediaQuery) && $this->mediaQuery !== '') {
|
||||
$result[] = $this->mediaQuery;
|
||||
}
|
||||
return $aResult;
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function getMediaQuery(): ?string
|
||||
{
|
||||
return $this->mediaQuery;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<array-key, Comment> $aComments
|
||||
* @return array<string, bool|int|float|string|array<mixed>|null>
|
||||
*
|
||||
* @return void
|
||||
* @internal
|
||||
*/
|
||||
public function addComments(array $aComments)
|
||||
public function getArrayRepresentation(): array
|
||||
{
|
||||
$this->aComments = array_merge($this->aComments, $aComments);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<array-key, Comment>
|
||||
*/
|
||||
public function getComments()
|
||||
{
|
||||
return $this->aComments;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<array-key, Comment> $aComments
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setComments(array $aComments)
|
||||
{
|
||||
$this->aComments = $aComments;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getMediaQuery()
|
||||
{
|
||||
return $this->sMediaQuery;
|
||||
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(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +1,45 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sabberworm\CSS\Property;
|
||||
|
||||
class KeyframeSelector extends Selector
|
||||
{
|
||||
/**
|
||||
* regexp for specificity calculations
|
||||
*
|
||||
* @var string
|
||||
* 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
|
||||
*/
|
||||
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';
|
||||
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';
|
||||
}
|
||||
|
||||
+212
-106
@@ -1,148 +1,254 @@
|
||||
<?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
|
||||
class Selector implements Renderable
|
||||
{
|
||||
/**
|
||||
* regexp for specificity calculations
|
||||
*
|
||||
* @var string
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
const NON_ID_ATTRIBUTES_AND_PSEUDO_CLASSES_RX = '/
|
||||
(\.[\w]+) # classes
|
||||
|
|
||||
\[(\w+) # attributes
|
||||
|
|
||||
(\:( # pseudo classes
|
||||
link|visited|active
|
||||
|hover|focus
|
||||
|lang
|
||||
|target
|
||||
|enabled|disabled|checked|indeterminate
|
||||
|root
|
||||
|nth-child|nth-last-child|nth-of-type|nth-last-of-type
|
||||
|first-child|last-child|first-of-type|last-of-type
|
||||
|only-child|only-of-type
|
||||
|empty|contains
|
||||
))
|
||||
/ix';
|
||||
use ShortClassNameProvider;
|
||||
|
||||
/**
|
||||
* regexp for specificity calculations
|
||||
*
|
||||
* @var string
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
const ELEMENTS_AND_PSEUDO_ELEMENTS_RX = '/
|
||||
((^|[\s\+\>\~]+)[\w]+ # elements
|
||||
|
|
||||
\:{1,2}( # pseudo-elements
|
||||
after|before|first-letter|first-line|selection
|
||||
))
|
||||
/ix';
|
||||
|
||||
/**
|
||||
* regexp for specificity calculations
|
||||
*
|
||||
* @var string
|
||||
*
|
||||
* @internal since 8.5.2
|
||||
*/
|
||||
const SELECTOR_VALIDATION_RX = '/
|
||||
public const SELECTOR_VALIDATION_RX = '/
|
||||
^(
|
||||
# not whitespace only
|
||||
(?!\\s*+$)
|
||||
(?:
|
||||
[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"]
|
||||
)*
|
||||
# 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 string
|
||||
* @var non-empty-list<Component>
|
||||
*/
|
||||
private $sSelector;
|
||||
private $components;
|
||||
|
||||
/**
|
||||
* @var int|null
|
||||
*/
|
||||
private $iSpecificity;
|
||||
|
||||
/**
|
||||
* @param string $sSelector
|
||||
*
|
||||
* @return bool
|
||||
*
|
||||
* @internal since V8.8.0
|
||||
*/
|
||||
public static function isValid($sSelector)
|
||||
public static function isValid(string $selector): bool
|
||||
{
|
||||
return preg_match(static::SELECTOR_VALIDATION_RX, $sSelector);
|
||||
// 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 string $sSelector
|
||||
* @param bool $bCalculateSpecificity @deprecated since V8.8.0, will be removed in V9.0.0
|
||||
* @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
|
||||
*/
|
||||
public function __construct($sSelector, $bCalculateSpecificity = false)
|
||||
final public function __construct($selector)
|
||||
{
|
||||
$this->setSelector($sSelector);
|
||||
if ($bCalculateSpecificity) {
|
||||
$this->getSpecificity();
|
||||
if (\is_string($selector)) {
|
||||
$this->setSelector($selector);
|
||||
} else {
|
||||
$this->setComponents($selector);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getSelector()
|
||||
{
|
||||
return $this->sSelector;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $sSelector
|
||||
* @param list<Comment> $comments
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setSelector($sSelector)
|
||||
{
|
||||
$this->sSelector = trim($sSelector);
|
||||
$this->iSpecificity = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
* @return non-empty-list<Component>
|
||||
*
|
||||
* @deprecated in V8.8.0, will be removed in V9.0.0. Use `render` instead.
|
||||
* @throws UnexpectedTokenException
|
||||
*/
|
||||
public function __toString()
|
||||
private static function parseComponents(ParserState $parserState, array &$comments = []): array
|
||||
{
|
||||
return $this->getSelector();
|
||||
}
|
||||
// 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);
|
||||
|
||||
/**
|
||||
* @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;
|
||||
$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 $this->iSpecificity;
|
||||
|
||||
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 = [];
|
||||
}
|
||||
}
|
||||
+6
-14
@@ -1,25 +1,17 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sabberworm\CSS;
|
||||
|
||||
interface Renderable
|
||||
{
|
||||
/**
|
||||
* @return string
|
||||
*
|
||||
* @deprecated in V8.8.0, will be removed in V9.0.0. Use `render` instead.
|
||||
*/
|
||||
public function __toString();
|
||||
public function render(OutputFormat $outputFormat): string;
|
||||
|
||||
/**
|
||||
* @param OutputFormat|null $oOutputFormat
|
||||
* @return array<string, bool|int|float|string|array<mixed>|null>
|
||||
*
|
||||
* @return string
|
||||
* @internal
|
||||
*/
|
||||
public function render($oOutputFormat);
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getLineNo();
|
||||
public function getArrayRepresentation(): array;
|
||||
}
|
||||
|
||||
+16
-375
@@ -1,383 +1,24 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sabberworm\CSS\Rule;
|
||||
|
||||
use Sabberworm\CSS\Comment\Comment;
|
||||
use Sabberworm\CSS\Comment\Commentable;
|
||||
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 Sabberworm\CSS\Property\Declaration;
|
||||
|
||||
/**
|
||||
* `Rule`s just have a string key (the rule) and a 'Value'.
|
||||
*
|
||||
* In CSS, `Rule`s are expressed as follows: “key: value[0][0] value[0][1], value[1][0] value[1][1];”
|
||||
*/
|
||||
class Rule implements Commentable, CSSElement, Positionable
|
||||
{
|
||||
use Position;
|
||||
use function Safe\class_alias;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $sRule;
|
||||
|
||||
/**
|
||||
* @var RuleValueList|string|null
|
||||
*/
|
||||
private $mValue;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $bIsImportant;
|
||||
|
||||
/**
|
||||
* @var array<int, int>
|
||||
*/
|
||||
private $aIeHack;
|
||||
|
||||
/**
|
||||
* @var array<array-key, Comment>
|
||||
*
|
||||
* @internal since 8.8.0
|
||||
*/
|
||||
protected $aComments;
|
||||
|
||||
/**
|
||||
* @param string $sRule
|
||||
* @param int $iLineNo
|
||||
* @param int $iColNo
|
||||
*/
|
||||
public function __construct($sRule, $iLineNo = 0, $iColNo = 0)
|
||||
{
|
||||
$this->sRule = $sRule;
|
||||
$this->mValue = null;
|
||||
$this->bIsImportant = false;
|
||||
$this->aIeHack = [];
|
||||
$this->setPosition($iLineNo, $iColNo);
|
||||
$this->aComments = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, Comment> $commentsBeforeRule
|
||||
*
|
||||
* @return Rule
|
||||
*
|
||||
* @throws UnexpectedEOFException
|
||||
* @throws UnexpectedTokenException
|
||||
*
|
||||
* @internal since V8.8.0
|
||||
*/
|
||||
public static function parse(ParserState $oParserState, $commentsBeforeRule = [])
|
||||
{
|
||||
$aComments = \array_merge($commentsBeforeRule, $oParserState->consumeWhiteSpace());
|
||||
$oRule = new Rule(
|
||||
$oParserState->parseIdentifier(!$oParserState->comes("--")),
|
||||
$oParserState->currentLine(),
|
||||
$oParserState->currentColumn()
|
||||
);
|
||||
$oRule->setComments($aComments);
|
||||
$oRule->addComments($oParserState->consumeWhiteSpace());
|
||||
$oParserState->consume(':');
|
||||
$oValue = Value::parseValue($oParserState, self::listDelimiterForRule($oRule->getRule()));
|
||||
$oRule->setValue($oValue);
|
||||
if ($oParserState->getSettings()->bLenientParsing) {
|
||||
while ($oParserState->comes('\\')) {
|
||||
$oParserState->consume('\\');
|
||||
$oRule->addIeHack($oParserState->consume());
|
||||
$oParserState->consumeWhiteSpace();
|
||||
}
|
||||
}
|
||||
$oParserState->consumeWhiteSpace();
|
||||
if ($oParserState->comes('!')) {
|
||||
$oParserState->consume('!');
|
||||
$oParserState->consumeWhiteSpace();
|
||||
$oParserState->consume('important');
|
||||
$oRule->setIsImportant(true);
|
||||
}
|
||||
$oParserState->consumeWhiteSpace();
|
||||
while ($oParserState->comes(';')) {
|
||||
$oParserState->consume(';');
|
||||
}
|
||||
|
||||
return $oRule;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of delimiters (or separators).
|
||||
* The first item is the innermost separator (or, put another way, the highest-precedence operator).
|
||||
* The sequence continues to the outermost separator (or lowest-precedence operator).
|
||||
*
|
||||
* @param string $sRule
|
||||
*
|
||||
* @return list<non-empty-string>
|
||||
*/
|
||||
private static function listDelimiterForRule($sRule)
|
||||
{
|
||||
if (preg_match('/^font($|-)/', $sRule)) {
|
||||
return [',', '/', ' '];
|
||||
}
|
||||
|
||||
switch ($sRule) {
|
||||
case 'src':
|
||||
return [' ', ','];
|
||||
default:
|
||||
return [',', ' ', '/'];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $sRule
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setRule($sRule)
|
||||
{
|
||||
$this->sRule = $sRule;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getRule()
|
||||
{
|
||||
return $this->sRule;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return RuleValueList|string|null
|
||||
*/
|
||||
public function getValue()
|
||||
{
|
||||
return $this->mValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param RuleValueList|string|null $mValue
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setValue($mValue)
|
||||
{
|
||||
$this->mValue = $mValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<array-key, array<array-key, RuleValueList>> $aSpaceSeparatedValues
|
||||
*
|
||||
* @return RuleValueList
|
||||
*
|
||||
* @deprecated will be removed in version 9.0
|
||||
* Old-Style 2-dimensional array given. Retained for (some) backwards-compatibility.
|
||||
* Use `setValue()` instead and wrap the value inside a RuleValueList if necessary.
|
||||
*/
|
||||
public function setValues(array $aSpaceSeparatedValues)
|
||||
{
|
||||
$oSpaceSeparatedList = null;
|
||||
if (count($aSpaceSeparatedValues) > 1) {
|
||||
$oSpaceSeparatedList = new RuleValueList(' ', $this->iLineNo);
|
||||
}
|
||||
foreach ($aSpaceSeparatedValues as $aCommaSeparatedValues) {
|
||||
$oCommaSeparatedList = null;
|
||||
if (count($aCommaSeparatedValues) > 1) {
|
||||
$oCommaSeparatedList = new RuleValueList(',', $this->iLineNo);
|
||||
}
|
||||
foreach ($aCommaSeparatedValues as $mValue) {
|
||||
if (!$oSpaceSeparatedList && !$oCommaSeparatedList) {
|
||||
$this->mValue = $mValue;
|
||||
return $mValue;
|
||||
}
|
||||
if ($oCommaSeparatedList) {
|
||||
$oCommaSeparatedList->addListComponent($mValue);
|
||||
} else {
|
||||
$oSpaceSeparatedList->addListComponent($mValue);
|
||||
}
|
||||
}
|
||||
if (!$oSpaceSeparatedList) {
|
||||
$this->mValue = $oCommaSeparatedList;
|
||||
return $oCommaSeparatedList;
|
||||
} else {
|
||||
$oSpaceSeparatedList->addListComponent($oCommaSeparatedList);
|
||||
}
|
||||
}
|
||||
$this->mValue = $oSpaceSeparatedList;
|
||||
return $oSpaceSeparatedList;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array<int, RuleValueList>>
|
||||
*
|
||||
* @deprecated will be removed in version 9.0
|
||||
* Old-Style 2-dimensional array returned. Retained for (some) backwards-compatibility.
|
||||
* Use `getValue()` instead and check for the existence of a (nested set of) ValueList object(s).
|
||||
*/
|
||||
public function getValues()
|
||||
{
|
||||
if (!$this->mValue instanceof RuleValueList) {
|
||||
return [[$this->mValue]];
|
||||
}
|
||||
if ($this->mValue->getListSeparator() === ',') {
|
||||
return [$this->mValue->getListComponents()];
|
||||
}
|
||||
$aResult = [];
|
||||
foreach ($this->mValue->getListComponents() as $mValue) {
|
||||
if (!$mValue instanceof RuleValueList || $mValue->getListSeparator() !== ',') {
|
||||
$aResult[] = [$mValue];
|
||||
continue;
|
||||
}
|
||||
if ($this->mValue->getListSeparator() === ' ' || count($aResult) === 0) {
|
||||
$aResult[] = [];
|
||||
}
|
||||
foreach ($mValue->getListComponents() as $mValue) {
|
||||
$aResult[count($aResult) - 1][] = $mValue;
|
||||
}
|
||||
}
|
||||
return $aResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a value to the existing value. Value will be appended if a `RuleValueList` exists of the given type.
|
||||
* Otherwise, the existing value will be wrapped by one.
|
||||
*
|
||||
* @param RuleValueList|array<int, RuleValueList> $mValue
|
||||
* @param string $sType
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function addValue($mValue, $sType = ' ')
|
||||
{
|
||||
if (!is_array($mValue)) {
|
||||
$mValue = [$mValue];
|
||||
}
|
||||
if (!$this->mValue instanceof RuleValueList || $this->mValue->getListSeparator() !== $sType) {
|
||||
$mCurrentValue = $this->mValue;
|
||||
$this->mValue = new RuleValueList($sType, $this->getLineNumber());
|
||||
if ($mCurrentValue) {
|
||||
$this->mValue->addListComponent($mCurrentValue);
|
||||
}
|
||||
}
|
||||
foreach ($mValue as $mValueItem) {
|
||||
$this->mValue->addListComponent($mValueItem);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $iModifier
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @deprecated since V8.8.0, will be removed in V9.0
|
||||
*/
|
||||
public function addIeHack($iModifier)
|
||||
{
|
||||
$this->aIeHack[] = $iModifier;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, int> $aModifiers
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @deprecated since V8.8.0, will be removed in V9.0
|
||||
*/
|
||||
public function setIeHack(array $aModifiers)
|
||||
{
|
||||
$this->aIeHack = $aModifiers;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, int>
|
||||
*
|
||||
* @deprecated since V8.8.0, will be removed in V9.0
|
||||
*/
|
||||
public function getIeHack()
|
||||
{
|
||||
return $this->aIeHack;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $bIsImportant
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setIsImportant($bIsImportant)
|
||||
{
|
||||
$this->bIsImportant = $bIsImportant;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function getIsImportant()
|
||||
{
|
||||
return $this->bIsImportant;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*
|
||||
* @deprecated in V8.8.0, will be removed in V9.0.0. Use `render` instead.
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
return $this->render(new OutputFormat());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param OutputFormat|null $oOutputFormat
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function render($oOutputFormat)
|
||||
{
|
||||
$sResult = "{$oOutputFormat->comments($this)}{$this->sRule}:{$oOutputFormat->spaceAfterRuleName()}";
|
||||
if ($this->mValue instanceof Value) { // Can also be a ValueList
|
||||
$sResult .= $this->mValue->render($oOutputFormat);
|
||||
} else {
|
||||
$sResult .= $this->mValue;
|
||||
}
|
||||
if (!empty($this->aIeHack)) {
|
||||
$sResult .= ' \\' . implode('\\', $this->aIeHack);
|
||||
}
|
||||
if ($this->bIsImportant) {
|
||||
$sResult .= ' !important';
|
||||
}
|
||||
$sResult .= ';';
|
||||
return $sResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<array-key, Comment> $aComments
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function addComments(array $aComments)
|
||||
{
|
||||
$this->aComments = array_merge($this->aComments, $aComments);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<array-key, Comment>
|
||||
*/
|
||||
public function getComments()
|
||||
{
|
||||
return $this->aComments;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<array-key, Comment> $aComments
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setComments(array $aComments)
|
||||
{
|
||||
$this->aComments = $aComments;
|
||||
// @phpstan-ignore function.impossibleType
|
||||
if (!\class_exists(Rule::class, false) && !\interface_exists(Rule::class, false)) {
|
||||
class_alias(Declaration::class, Rule::class);
|
||||
// The test is expected to evaluate to false,
|
||||
// but allows for the deprecation notice to be picked up by IDEs like PHPStorm.
|
||||
// @phpstan-ignore booleanNot.alwaysTrue, booleanAnd.alwaysTrue, function.impossibleType
|
||||
if (!\class_exists(Rule::class, false) && !\interface_exists(Rule::class, false)) {
|
||||
/**
|
||||
* @deprecated in v9.2, will be removed in v10.0. Use `Property\Declaration` instead, which is a direct
|
||||
* replacement.
|
||||
*/
|
||||
class Rule {}
|
||||
}
|
||||
}
|
||||
|
||||
+44
-43
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sabberworm\CSS\RuleSet;
|
||||
|
||||
use Sabberworm\CSS\OutputFormat;
|
||||
@@ -14,68 +16,67 @@ use Sabberworm\CSS\Property\AtRule;
|
||||
class AtRuleSet extends RuleSet implements AtRule
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
* @var non-empty-string
|
||||
*/
|
||||
private $sType;
|
||||
private $type;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $sArgs;
|
||||
private $arguments;
|
||||
|
||||
/**
|
||||
* @param string $sType
|
||||
* @param string $sArgs
|
||||
* @param int $iLineNo
|
||||
* @param non-empty-string $type
|
||||
* @param int<1, max>|null $lineNumber
|
||||
*/
|
||||
public function __construct($sType, $sArgs = '', $iLineNo = 0)
|
||||
public function __construct(string $type, string $arguments = '', ?int $lineNumber = null)
|
||||
{
|
||||
parent::__construct($iLineNo);
|
||||
$this->sType = $sType;
|
||||
$this->sArgs = $sArgs;
|
||||
parent::__construct($lineNumber);
|
||||
$this->type = $type;
|
||||
$this->arguments = $arguments;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
* @return non-empty-string
|
||||
*/
|
||||
public function atRuleName()
|
||||
public function atRuleName(): string
|
||||
{
|
||||
return $this->sType;
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
public function atRuleArgs(): string
|
||||
{
|
||||
return $this->arguments;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
* @return non-empty-string
|
||||
*/
|
||||
public function atRuleArgs()
|
||||
public function render(OutputFormat $outputFormat): string
|
||||
{
|
||||
return $this->sArgs;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*
|
||||
* @deprecated in V8.8.0, will be removed in V9.0.0. Use `render` instead.
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
return $this->render(new OutputFormat());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param OutputFormat|null $oOutputFormat
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function render($oOutputFormat)
|
||||
{
|
||||
$sResult = $oOutputFormat->comments($this);
|
||||
$sArgs = $this->sArgs;
|
||||
if ($sArgs) {
|
||||
$sArgs = ' ' . $sArgs;
|
||||
$formatter = $outputFormat->getFormatter();
|
||||
$result = $formatter->comments($this);
|
||||
$arguments = $this->arguments;
|
||||
if ($arguments !== '') {
|
||||
$arguments = ' ' . $arguments;
|
||||
}
|
||||
$sResult .= "@{$this->sType}$sArgs{$oOutputFormat->spaceBeforeOpeningBrace()}{";
|
||||
$sResult .= $this->renderRules($oOutputFormat);
|
||||
$sResult .= '}';
|
||||
return $sResult;
|
||||
$result .= "@{$this->type}$arguments{$formatter->spaceBeforeOpeningBrace()}{";
|
||||
$result .= $this->renderDeclarations($outputFormat);
|
||||
$result .= '}';
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, bool|int|float|string|array<mixed>|null>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public function getArrayRepresentation(): array
|
||||
{
|
||||
$arrayRepresentation = parent::getArrayRepresentation();
|
||||
$arrayRepresentation['atRuleName'] = $this->type;
|
||||
$arrayRepresentation['arguments'] = $this->arguments;
|
||||
|
||||
return $arrayRepresentation;
|
||||
}
|
||||
}
|
||||
|
||||
+171
-736
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sabberworm\CSS\RuleSet;
|
||||
|
||||
use Sabberworm\CSS\Property\Declaration;
|
||||
|
||||
/**
|
||||
* Represents a CSS item that contains `Declaration`s, defining the methods to manipulate them.
|
||||
*/
|
||||
interface DeclarationList
|
||||
{
|
||||
public function addDeclaration(Declaration $declarationToAdd, ?Declaration $sibling = null): void;
|
||||
|
||||
public function removeDeclaration(Declaration $declarationToRemove): void;
|
||||
|
||||
public function removeMatchingDeclarations(string $searchPattern): void;
|
||||
|
||||
public function removeAllDeclarations(): void;
|
||||
|
||||
/**
|
||||
* @param array<Declaration> $declarations
|
||||
*/
|
||||
public function setDeclarations(array $declarations): void;
|
||||
|
||||
/**
|
||||
* @return array<int<0, max>, Declaration>
|
||||
*/
|
||||
public function getDeclarations(?string $searchPattern = null): array;
|
||||
|
||||
/**
|
||||
* @return array<string, Declaration>
|
||||
*/
|
||||
public function getDeclarationsAssociative(?string $searchPattern = null): array;
|
||||
|
||||
/**
|
||||
* @deprecated in v9.2, will be removed in v10.0; use `addDeclaration()` instead.
|
||||
*/
|
||||
public function addRule(Declaration $declarationToAdd, ?Declaration $sibling = null): void;
|
||||
|
||||
/**
|
||||
* @deprecated in v9.2, will be removed in v10.0; use `removeDeclaration()` instead.
|
||||
*/
|
||||
public function removeRule(Declaration $declarationToRemove): void;
|
||||
|
||||
/**
|
||||
* @deprecated in v9.2, will be removed in v10.0; use `removeMatchingDeclarations()` instead.
|
||||
*/
|
||||
public function removeMatchingRules(string $searchPattern): void;
|
||||
|
||||
/**
|
||||
* @deprecated in v9.2, will be removed in v10.0; use `removeAllDeclarations()` instead.
|
||||
*/
|
||||
public function removeAllRules(): void;
|
||||
|
||||
/**
|
||||
* @param array<Declaration> $declarations
|
||||
*
|
||||
* @deprecated in v9.2, will be removed in v10.0; use `setDeclarations()` instead.
|
||||
*/
|
||||
public function setRules(array $declarations): void;
|
||||
|
||||
/**
|
||||
* @return array<int<0, max>, Declaration>
|
||||
*
|
||||
* @deprecated in v9.2, will be removed in v10.0; use `getDeclarations()` instead.
|
||||
*/
|
||||
public function getRules(?string $searchPattern = null): array;
|
||||
|
||||
/**
|
||||
* @return array<string, Declaration>
|
||||
*
|
||||
* @deprecated in v9.2, will be removed in v10.0; use `getDeclarationsAssociative()` instead.
|
||||
*/
|
||||
public function getRulesAssoc(?string $searchPattern = null): array;
|
||||
}
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sabberworm\CSS\RuleSet;
|
||||
|
||||
use Sabberworm\CSS\Property\Declaration;
|
||||
|
||||
/**
|
||||
* Provides a mapping of the deprecated methods in a `DeclarationList` to their renamed replacements.
|
||||
*/
|
||||
trait LegacyDeclarationListMethods
|
||||
{
|
||||
/**
|
||||
* @deprecated in v9.2, will be removed in v10.0; use `addDeclaration()` instead.
|
||||
*/
|
||||
public function addRule(Declaration $declarationToAdd, ?Declaration $sibling = null): void
|
||||
{
|
||||
$this->addDeclaration($declarationToAdd, $sibling);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated in v9.2, will be removed in v10.0; use `removeDeclaration()` instead.
|
||||
*/
|
||||
public function removeRule(Declaration $declarationToRemove): void
|
||||
{
|
||||
$this->removeDeclaration($declarationToRemove);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated in v9.2, will be removed in v10.0; use `removeMatchingDeclarations()` instead.
|
||||
*/
|
||||
public function removeMatchingRules(string $searchPattern): void
|
||||
{
|
||||
$this->removeMatchingDeclarations($searchPattern);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated in v9.2, will be removed in v10.0; use `removeAllDeclarations()` instead.
|
||||
*/
|
||||
public function removeAllRules(): void
|
||||
{
|
||||
$this->removeAllDeclarations();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<Declaration> $declarations
|
||||
*
|
||||
* @deprecated in v9.2, will be removed in v10.0; use `setDeclarations()` instead.
|
||||
*/
|
||||
public function setRules(array $declarations): void
|
||||
{
|
||||
$this->setDeclarations($declarations);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int<0, max>, Declaration>
|
||||
*
|
||||
* @deprecated in v9.2, will be removed in v10.0; use `getDeclarations()` instead.
|
||||
*/
|
||||
public function getRules(?string $searchPattern = null): array
|
||||
{
|
||||
return $this->getDeclarations($searchPattern);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, Declaration>
|
||||
*
|
||||
* @deprecated in v9.2, will be removed in v10.0; use `getDeclarationsAssociative()` instead.
|
||||
*/
|
||||
public function getRulesAssoc(?string $searchPattern = null): array
|
||||
{
|
||||
return $this->getDeclarationsAssociative($searchPattern);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sabberworm\CSS\RuleSet;
|
||||
|
||||
use function Safe\class_alias;
|
||||
|
||||
// @phpstan-ignore function.impossibleType
|
||||
if (!\class_exists(RuleContainer::class, false) && !\interface_exists(RuleContainer::class, false)) {
|
||||
class_alias(DeclarationList::class, RuleContainer::class);
|
||||
// The test is expected to evaluate to false,
|
||||
// but allows for the deprecation notice to be picked up by IDEs like PHPStorm.
|
||||
// @phpstan-ignore booleanNot.alwaysTrue, booleanAnd.alwaysTrue, function.impossibleType
|
||||
if (!\class_exists(RuleContainer::class, false) && !\interface_exists(RuleContainer::class, false)) {
|
||||
/**
|
||||
* @deprecated in v9.2, will be removed in v10.0. Use `DeclarationList` instead, which is a direct replacement.
|
||||
*/
|
||||
interface RuleContainer {}
|
||||
}
|
||||
}
|
||||
+243
-194
@@ -1,88 +1,85 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sabberworm\CSS\RuleSet;
|
||||
|
||||
use Sabberworm\CSS\Comment\Comment;
|
||||
use Sabberworm\CSS\Comment\Commentable;
|
||||
use Sabberworm\CSS\Comment\CommentContainer;
|
||||
use Sabberworm\CSS\CSSElement;
|
||||
use Sabberworm\CSS\CSSList\CSSListItem;
|
||||
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\Renderable;
|
||||
use Sabberworm\CSS\Rule\Rule;
|
||||
use Sabberworm\CSS\Property\Declaration;
|
||||
use Sabberworm\CSS\ShortClassNameProvider;
|
||||
|
||||
/**
|
||||
* This class is a container for individual 'Rule's.
|
||||
* This class is a container for individual `Declaration`s.
|
||||
*
|
||||
* The most common form of a rule set is one constrained by a selector, i.e., a `DeclarationBlock`.
|
||||
* However, unknown `AtRule`s (like `@font-face`) are rule sets as well.
|
||||
*
|
||||
* If you want to manipulate a `RuleSet`, use the methods `addRule(Rule $rule)`, `getRules()` and `removeRule($rule)`
|
||||
* (which accepts either a `Rule` or a rule name; optionally suffixed by a dash to remove all related rules).
|
||||
* If you want to manipulate a `RuleSet`,
|
||||
* use the methods `addDeclaration()`, `getDeclarations()`, `removeDeclaration()`, `removeMatchingDeclarations()`, etc.
|
||||
*
|
||||
* Note that `CSSListItem` extends both `Commentable` and `Renderable`, so those interfaces must also be implemented.
|
||||
*/
|
||||
abstract class RuleSet implements CSSElement, Commentable, Positionable
|
||||
class RuleSet implements CSSElement, CSSListItem, Positionable, DeclarationList
|
||||
{
|
||||
use CommentContainer;
|
||||
use LegacyDeclarationListMethods;
|
||||
use Position;
|
||||
use ShortClassNameProvider;
|
||||
|
||||
/**
|
||||
* the rules in this rule set, using the property name as the key,
|
||||
* with potentially multiple rules per property name.
|
||||
* the declarations in this rule set, using the property name as the key,
|
||||
* with potentially multiple declarations per property name.
|
||||
*
|
||||
* @var array<string, array<int<0, max>, Rule>>
|
||||
* @var array<string, array<int<0, max>, Declaration>>
|
||||
*/
|
||||
private $aRules;
|
||||
private $declarations = [];
|
||||
|
||||
/**
|
||||
* @var array<array-key, Comment>
|
||||
*
|
||||
* @internal since 8.8.0
|
||||
* @param int<1, max>|null $lineNumber
|
||||
*/
|
||||
protected $aComments;
|
||||
|
||||
/**
|
||||
* @param int $iLineNo
|
||||
*/
|
||||
public function __construct($iLineNo = 0)
|
||||
public function __construct(?int $lineNumber = null)
|
||||
{
|
||||
$this->aRules = [];
|
||||
$this->setPosition($iLineNo);
|
||||
$this->aComments = [];
|
||||
$this->setPosition($lineNumber);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*
|
||||
* @throws UnexpectedTokenException
|
||||
* @throws UnexpectedEOFException
|
||||
*
|
||||
* @internal since V8.8.0
|
||||
*/
|
||||
public static function parseRuleSet(ParserState $oParserState, RuleSet $oRuleSet)
|
||||
public static function parseRuleSet(ParserState $parserState, RuleSet $ruleSet): void
|
||||
{
|
||||
while ($oParserState->comes(';')) {
|
||||
$oParserState->consume(';');
|
||||
while ($parserState->comes(';')) {
|
||||
$parserState->consume(';');
|
||||
}
|
||||
while (true) {
|
||||
$commentsBeforeRule = $oParserState->consumeWhiteSpace();
|
||||
if ($oParserState->comes('}')) {
|
||||
$commentsBeforeDeclaration = [];
|
||||
$parserState->consumeWhiteSpace($commentsBeforeDeclaration);
|
||||
if ($parserState->comes('}')) {
|
||||
break;
|
||||
}
|
||||
$oRule = null;
|
||||
if ($oParserState->getSettings()->bLenientParsing) {
|
||||
$declaration = null;
|
||||
if ($parserState->getSettings()->usesLenientParsing()) {
|
||||
try {
|
||||
$oRule = Rule::parse($oParserState, $commentsBeforeRule);
|
||||
$declaration = Declaration::parse($parserState, $commentsBeforeDeclaration);
|
||||
} catch (UnexpectedTokenException $e) {
|
||||
try {
|
||||
$sConsume = $oParserState->consumeUntil(["\n", ";", '}'], true);
|
||||
$consumedText = $parserState->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);
|
||||
if ($parserState->streql(\substr($consumedText, -1), '}')) {
|
||||
$parserState->backtrack(1);
|
||||
} else {
|
||||
while ($oParserState->comes(';')) {
|
||||
$oParserState->consume(';');
|
||||
while ($parserState->comes(';')) {
|
||||
$parserState->consume(';');
|
||||
}
|
||||
}
|
||||
} catch (UnexpectedTokenException $e) {
|
||||
@@ -91,267 +88,319 @@ abstract class RuleSet implements CSSElement, Commentable, Positionable
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$oRule = Rule::parse($oParserState, $commentsBeforeRule);
|
||||
$declaration = Declaration::parse($parserState, $commentsBeforeDeclaration);
|
||||
}
|
||||
if ($oRule) {
|
||||
$oRuleSet->addRule($oRule);
|
||||
if ($declaration instanceof Declaration) {
|
||||
$ruleSet->addDeclaration($declaration);
|
||||
}
|
||||
}
|
||||
$oParserState->consume('}');
|
||||
$parserState->consume('}');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Rule|null $oSibling
|
||||
*
|
||||
* @return void
|
||||
* @throws \UnexpectedValueException
|
||||
* if the last `Declaration` is needed as a basis for setting position, but does not have a valid position,
|
||||
* which should never happen
|
||||
*/
|
||||
public function addRule(Rule $oRule, $oSibling = null)
|
||||
public function addDeclaration(Declaration $declarationToAdd, ?Declaration $sibling = null): void
|
||||
{
|
||||
$sRule = $oRule->getRule();
|
||||
if (!isset($this->aRules[$sRule])) {
|
||||
$this->aRules[$sRule] = [];
|
||||
$propertyName = $declarationToAdd->getPropertyName();
|
||||
if (!isset($this->declarations[$propertyName])) {
|
||||
$this->declarations[$propertyName] = [];
|
||||
}
|
||||
|
||||
$iPosition = count($this->aRules[$sRule]);
|
||||
$position = \count($this->declarations[$propertyName]);
|
||||
|
||||
if ($oSibling !== null) {
|
||||
$iSiblingPos = array_search($oSibling, $this->aRules[$sRule], true);
|
||||
if ($iSiblingPos !== false) {
|
||||
$iPosition = $iSiblingPos;
|
||||
$oRule->setPosition($oSibling->getLineNo(), $oSibling->getColNo() - 1);
|
||||
}
|
||||
}
|
||||
if ($oRule->getLineNumber() === null) {
|
||||
//this node is added manually, give it the next best line
|
||||
$columnNumber = $oRule->getColNo();
|
||||
$rules = $this->getRules();
|
||||
$pos = count($rules);
|
||||
if ($pos > 0) {
|
||||
$last = $rules[$pos - 1];
|
||||
$oRule->setPosition($last->getLineNo() + 1, $columnNumber);
|
||||
if ($sibling !== null) {
|
||||
$siblingPosition = \array_search($sibling, $this->declarations[$propertyName], true);
|
||||
if ($siblingPosition !== false) {
|
||||
$siblingIsInSet = true;
|
||||
$position = $siblingPosition;
|
||||
} else {
|
||||
$oRule->setPosition(1, $columnNumber);
|
||||
$siblingIsInSet = $this->hasDeclaration($sibling);
|
||||
if ($siblingIsInSet) {
|
||||
// Maintain ordering within `$this->declarations[$propertyName]`
|
||||
// by inserting before first `Declaration` with a same-or-later position than the sibling.
|
||||
foreach ($this->declarations[$propertyName] as $index => $declaration) {
|
||||
if (self::comparePositionable($declaration, $sibling) >= 0) {
|
||||
$position = $index;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($siblingIsInSet) {
|
||||
// Increment column number of all existing declarations on same line, starting at sibling
|
||||
$siblingLineNumber = $sibling->getLineNumber();
|
||||
$siblingColumnNumber = $sibling->getColumnNumber();
|
||||
foreach ($this->declarations as $declarationsForAProperty) {
|
||||
foreach ($declarationsForAProperty as $declaration) {
|
||||
if (
|
||||
$declaration->getLineNumber() === $siblingLineNumber
|
||||
&& $declaration->getColumnNumber() >= $siblingColumnNumber
|
||||
) {
|
||||
$declaration->setPosition($siblingLineNumber, $declaration->getColumnNumber() + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
$declarationToAdd->setPosition($siblingLineNumber, $siblingColumnNumber);
|
||||
}
|
||||
} elseif ($oRule->getColumnNumber() === null) {
|
||||
$oRule->setPosition($oRule->getLineNumber(), 0);
|
||||
}
|
||||
|
||||
array_splice($this->aRules[$sRule], $iPosition, 0, [$oRule]);
|
||||
if ($declarationToAdd->getLineNumber() === null) {
|
||||
//this node is added manually, give it the next best line
|
||||
$columnNumber = $declarationToAdd->getColumnNumber() ?? 0;
|
||||
$declarations = $this->getDeclarations();
|
||||
$declarationsCount = \count($declarations);
|
||||
if ($declarationsCount > 0) {
|
||||
$last = $declarations[$declarationsCount - 1];
|
||||
$lastsLineNumber = $last->getLineNumber();
|
||||
if (!\is_int($lastsLineNumber)) {
|
||||
throw new \UnexpectedValueException(
|
||||
'A Declaration without a line number was found during addDeclaration',
|
||||
1750718399
|
||||
);
|
||||
}
|
||||
$declarationToAdd->setPosition($lastsLineNumber + 1, $columnNumber);
|
||||
} else {
|
||||
$declarationToAdd->setPosition(1, $columnNumber);
|
||||
}
|
||||
} elseif ($declarationToAdd->getColumnNumber() === null) {
|
||||
$declarationToAdd->setPosition($declarationToAdd->getLineNumber(), 0);
|
||||
}
|
||||
|
||||
\array_splice($this->declarations[$propertyName], $position, 0, [$declarationToAdd]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all rules matching the given rule name
|
||||
* Returns all declarations matching the given property name
|
||||
*
|
||||
* @example $oRuleSet->getRules('font') // returns array(0 => $oRule, …) or array().
|
||||
* @example $ruleSet->getDeclarations('font') // returns array(0 => $declaration, …) or array().
|
||||
*
|
||||
* @example $oRuleSet->getRules('font-')
|
||||
* //returns an array of all rules either beginning with font- or matching font.
|
||||
* @example $ruleSet->getDeclarations('font-')
|
||||
* //returns an array of all declarations 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
|
||||
* @param string|null $searchPattern
|
||||
* Pattern to search for. If null, returns all declarations.
|
||||
* If the pattern ends with a dash, all declarations starting with the pattern are returned
|
||||
* as well as one matching the pattern with the dash excluded.
|
||||
* Passing a `Rule` for this parameter is deprecated in version 8.9.0, and will not work from v9.0.
|
||||
* Call `getRules($rule->getRule())` instead.
|
||||
*
|
||||
* @return array<int, Rule>
|
||||
* @return array<int<0, max>, Declaration>
|
||||
*/
|
||||
public function getRules($mRule = null)
|
||||
public function getDeclarations(?string $searchPattern = null): array
|
||||
{
|
||||
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.
|
||||
$result = [];
|
||||
foreach ($this->declarations as $propertyName => $declarations) {
|
||||
// Either no search pattern was given
|
||||
// or the search pattern matches the found declaration's property name exactly
|
||||
// or the search pattern ends in “-”
|
||||
// ... and the found declaration's property name starts with the search pattern
|
||||
if (
|
||||
!$mRule || $sName === $mRule
|
||||
$searchPattern === null || $propertyName === $searchPattern
|
||||
|| (
|
||||
strrpos($mRule, '-') === strlen($mRule) - strlen('-')
|
||||
&& (strpos($sName, $mRule) === 0 || $sName === substr($mRule, 0, -1))
|
||||
\strrpos($searchPattern, '-') === \strlen($searchPattern) - \strlen('-')
|
||||
&& (\strpos($propertyName, $searchPattern) === 0
|
||||
|| $propertyName === \substr($searchPattern, 0, -1))
|
||||
)
|
||||
) {
|
||||
$aResult = array_merge($aResult, $aRules);
|
||||
$result = \array_merge($result, $declarations);
|
||||
}
|
||||
}
|
||||
usort($aResult, function (Rule $first, Rule $second) {
|
||||
if ($first->getLineNo() === $second->getLineNo()) {
|
||||
return $first->getColNo() - $second->getColNo();
|
||||
}
|
||||
return $first->getLineNo() - $second->getLineNo();
|
||||
});
|
||||
return $aResult;
|
||||
\usort($result, [self::class, 'comparePositionable']);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides all the rules of this set.
|
||||
* Overrides all the declarations of this set.
|
||||
*
|
||||
* @param array<array-key, Rule> $aRules The rules to override with.
|
||||
*
|
||||
* @return void
|
||||
* @param array<Declaration> $declarations
|
||||
*/
|
||||
public function setRules(array $aRules)
|
||||
public function setDeclarations(array $declarations): void
|
||||
{
|
||||
$this->aRules = [];
|
||||
foreach ($aRules as $rule) {
|
||||
$this->addRule($rule);
|
||||
$this->declarations = [];
|
||||
foreach ($declarations as $declaration) {
|
||||
$this->addDeclaration($declaration);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* Returns all declarations with property names matching the given pattern and returns them in an associative array
|
||||
* with the property names 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.
|
||||
* containing the rgba-valued declaration while `getDeclarations()` 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
|
||||
* @param string|null $searchPattern
|
||||
* Pattern to search for. If null, returns all declarations. If the pattern ends with a dash,
|
||||
* all declarations starting with the pattern are returned as well as one matching the pattern with the dash
|
||||
* excluded.
|
||||
* Passing a `Rule` for this parameter is deprecated in version 8.9.0, and will not work from v9.0.
|
||||
* Call `getRulesAssoc($rule->getRule())` instead.
|
||||
*
|
||||
* @return array<string, Rule>
|
||||
* @return array<string, Declaration>
|
||||
*/
|
||||
public function getRulesAssoc($mRule = null)
|
||||
public function getDeclarationsAssociative(?string $searchPattern = null): array
|
||||
{
|
||||
/** @var array<string, Rule> $aResult */
|
||||
$aResult = [];
|
||||
foreach ($this->getRules($mRule) as $oRule) {
|
||||
$aResult[$oRule->getRule()] = $oRule;
|
||||
/** @var array<string, Declaration> $result */
|
||||
$result = [];
|
||||
foreach ($this->getDeclarations($searchPattern) as $declaration) {
|
||||
$result[$declaration->getPropertyName()] = $declaration;
|
||||
}
|
||||
return $aResult;
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a `Rule` from this `RuleSet` by identity.
|
||||
*
|
||||
* @param Rule|string|null $mRule
|
||||
* `Rule` to remove.
|
||||
* Passing a `string` or `null` is deprecated in version 8.9.0, and will no longer work from v9.0.
|
||||
* Use `removeMatchingRules()` or `removeAllRules()` instead.
|
||||
* Removes a `Declaration` from this `RuleSet` by identity.
|
||||
*/
|
||||
public function removeRule($mRule)
|
||||
public function removeDeclaration(Declaration $declarationToRemove): void
|
||||
{
|
||||
if ($mRule instanceof Rule) {
|
||||
$sRule = $mRule->getRule();
|
||||
if (!isset($this->aRules[$sRule])) {
|
||||
return;
|
||||
$nameOfPropertyToRemove = $declarationToRemove->getPropertyName();
|
||||
if (!isset($this->declarations[$nameOfPropertyToRemove])) {
|
||||
return;
|
||||
}
|
||||
foreach ($this->declarations[$nameOfPropertyToRemove] as $key => $declaration) {
|
||||
if ($declaration === $declarationToRemove) {
|
||||
unset($this->declarations[$nameOfPropertyToRemove][$key]);
|
||||
}
|
||||
foreach ($this->aRules[$sRule] as $iKey => $oRule) {
|
||||
if ($oRule === $mRule) {
|
||||
unset($this->aRules[$sRule][$iKey]);
|
||||
}
|
||||
}
|
||||
} elseif ($mRule !== null) {
|
||||
$this->removeMatchingRules($mRule);
|
||||
} else {
|
||||
$this->removeAllRules();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes rules by property name or search pattern.
|
||||
* Removes declarations by property name or search pattern.
|
||||
*
|
||||
* @param string $searchPattern
|
||||
* pattern to remove.
|
||||
* If the pattern ends in a dash,
|
||||
* all rules starting with the pattern are removed as well as one matching the pattern with the dash
|
||||
* all declarations starting with the pattern are removed as well as one matching the pattern with the dash
|
||||
* excluded.
|
||||
*/
|
||||
public function removeMatchingRules($searchPattern)
|
||||
public function removeMatchingDeclarations(string $searchPattern): void
|
||||
{
|
||||
foreach ($this->aRules as $propertyName => $rules) {
|
||||
// Either the search rule matches the found rule exactly
|
||||
// or the search rule ends in “-” and the found rule starts with the search rule or equals it
|
||||
// (without the trailing dash).
|
||||
foreach ($this->declarations as $propertyName => $declarations) {
|
||||
// Either the search pattern matches the found declaration's property name exactly
|
||||
// or the search pattern ends in “-” and the found declaration's property name starts with the search
|
||||
// pattern or equals it (without the trailing dash).
|
||||
if (
|
||||
$propertyName === $searchPattern
|
||||
|| (\strrpos($searchPattern, '-') === \strlen($searchPattern) - \strlen('-')
|
||||
&& (\strpos($propertyName, $searchPattern) === 0
|
||||
|| $propertyName === \substr($searchPattern, 0, -1)))
|
||||
) {
|
||||
unset($this->aRules[$propertyName]);
|
||||
unset($this->declarations[$propertyName]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function removeAllRules()
|
||||
public function removeAllDeclarations(): void
|
||||
{
|
||||
$this->aRules = [];
|
||||
$this->declarations = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*
|
||||
* @deprecated in V8.8.0, will be removed in V9.0.0. Use `render` instead.
|
||||
* @internal
|
||||
*/
|
||||
public function __toString()
|
||||
public function render(OutputFormat $outputFormat): string
|
||||
{
|
||||
return $this->render(new OutputFormat());
|
||||
return $this->renderDeclarations($outputFormat);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
protected function renderRules(OutputFormat $oOutputFormat)
|
||||
protected function renderDeclarations(OutputFormat $outputFormat): string
|
||||
{
|
||||
$sResult = '';
|
||||
$bIsFirst = true;
|
||||
$oNextLevel = $oOutputFormat->nextLevel();
|
||||
foreach ($this->getRules() as $oRule) {
|
||||
$sRendered = $oNextLevel->safely(function () use ($oRule, $oNextLevel) {
|
||||
return $oRule->render($oNextLevel);
|
||||
});
|
||||
if ($sRendered === null) {
|
||||
$result = '';
|
||||
$isFirst = true;
|
||||
$nextLevelFormat = $outputFormat->nextLevel();
|
||||
foreach ($this->getDeclarations() as $declaration) {
|
||||
$nextLevelFormatter = $nextLevelFormat->getFormatter();
|
||||
$renderedDeclaration = $nextLevelFormatter->safely(
|
||||
static function () use ($declaration, $nextLevelFormat): string {
|
||||
return $declaration->render($nextLevelFormat);
|
||||
}
|
||||
);
|
||||
if ($renderedDeclaration === null) {
|
||||
continue;
|
||||
}
|
||||
if ($bIsFirst) {
|
||||
$bIsFirst = false;
|
||||
$sResult .= $oNextLevel->spaceBeforeRules();
|
||||
if ($isFirst) {
|
||||
$isFirst = false;
|
||||
$result .= $nextLevelFormatter->spaceBeforeRules();
|
||||
} else {
|
||||
$sResult .= $oNextLevel->spaceBetweenRules();
|
||||
$result .= $nextLevelFormatter->spaceBetweenRules();
|
||||
}
|
||||
$sResult .= $sRendered;
|
||||
$result .= $renderedDeclaration;
|
||||
}
|
||||
|
||||
if (!$bIsFirst) {
|
||||
$formatter = $outputFormat->getFormatter();
|
||||
if (!$isFirst) {
|
||||
// Had some output
|
||||
$sResult .= $oOutputFormat->spaceAfterRules();
|
||||
$result .= $formatter->spaceAfterRules();
|
||||
}
|
||||
|
||||
return $oOutputFormat->removeLastSemicolon($sResult);
|
||||
return $formatter->removeLastSemicolon($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, Comment> $aComments
|
||||
* @return array<string, bool|int|float|string|array<mixed>|null>
|
||||
*
|
||||
* @return void
|
||||
* @internal
|
||||
*/
|
||||
public function addComments(array $aComments)
|
||||
public function getArrayRepresentation(): array
|
||||
{
|
||||
$this->aComments = array_merge($this->aComments, $aComments);
|
||||
$declarationsArrayRepresentation = [];
|
||||
foreach ($this->declarations as $propertyName => $declarationsForOneProperty) {
|
||||
$declarationsArrayRepresentation[$propertyName] = \array_map(
|
||||
function (Declaration $declaration): array {
|
||||
return $declaration->getArrayRepresentation();
|
||||
},
|
||||
$declarationsForOneProperty
|
||||
);
|
||||
}
|
||||
|
||||
return [
|
||||
'class' => $this->getShortClassName(),
|
||||
'declarations' => $declarationsArrayRepresentation,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, Comment>
|
||||
*/
|
||||
public function getComments()
|
||||
{
|
||||
return $this->aComments;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, Comment> $aComments
|
||||
* @return int negative if `$first` is before `$second`; zero if they have the same position; positive otherwise
|
||||
*
|
||||
* @return void
|
||||
* @throws \UnexpectedValueException if either argument does not have a valid position, which should never happen
|
||||
*/
|
||||
public function setComments(array $aComments)
|
||||
private static function comparePositionable(Positionable $first, Positionable $second): int
|
||||
{
|
||||
$this->aComments = $aComments;
|
||||
$firstsLineNumber = $first->getLineNumber();
|
||||
$secondsLineNumber = $second->getLineNumber();
|
||||
if (!\is_int($firstsLineNumber) || !\is_int($secondsLineNumber)) {
|
||||
throw new \UnexpectedValueException(
|
||||
'A Declaration without a line number was passed to comparePositionable',
|
||||
1750637683
|
||||
);
|
||||
}
|
||||
|
||||
if ($firstsLineNumber === $secondsLineNumber) {
|
||||
$firstsColumnNumber = $first->getColumnNumber();
|
||||
$secondsColumnNumber = $second->getColumnNumber();
|
||||
if (!\is_int($firstsColumnNumber) || !\is_int($secondsColumnNumber)) {
|
||||
throw new \UnexpectedValueException(
|
||||
'A Declaration without a column number was passed to comparePositionable',
|
||||
1750637761
|
||||
);
|
||||
}
|
||||
return $firstsColumnNumber - $secondsColumnNumber;
|
||||
}
|
||||
|
||||
return $firstsLineNumber - $secondsLineNumber;
|
||||
}
|
||||
|
||||
private function hasDeclaration(Declaration $declaration): bool
|
||||
{
|
||||
foreach ($this->declarations as $declarationsForAProperty) {
|
||||
if (\in_array($declaration, $declarationsForAProperty, true)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
+49
-31
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sabberworm\CSS;
|
||||
|
||||
/**
|
||||
@@ -16,38 +18,29 @@ class Settings
|
||||
* and `mb_strpos` functions. Otherwise, the normal (ASCII-Only) functions will be used.
|
||||
*
|
||||
* @var bool
|
||||
*
|
||||
* @internal since 8.8.0, will be made private in 9.0.0
|
||||
*/
|
||||
public $bMultibyteSupport;
|
||||
private $multibyteSupport;
|
||||
|
||||
/**
|
||||
* The default charset for the CSS if no `@charset` declaration is found. Defaults to utf-8.
|
||||
*
|
||||
* @var string
|
||||
*
|
||||
* @internal since 8.8.0, will be made private in 9.0.0
|
||||
* @var non-empty-string
|
||||
*/
|
||||
public $sDefaultCharset = 'utf-8';
|
||||
private $defaultCharset = 'utf-8';
|
||||
|
||||
/**
|
||||
* Whether the parser silently ignore invalid rules instead of choking on them.
|
||||
*
|
||||
* @var bool
|
||||
*
|
||||
* @internal since 8.8.0, will be made private in 9.0.0
|
||||
*/
|
||||
public $bLenientParsing = true;
|
||||
private $lenientParsing = true;
|
||||
|
||||
private function __construct()
|
||||
{
|
||||
$this->bMultibyteSupport = extension_loaded('mbstring');
|
||||
$this->multibyteSupport = \extension_loaded('mbstring');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return self new instance
|
||||
*/
|
||||
public static function create()
|
||||
public static function create(): self
|
||||
{
|
||||
return new Settings();
|
||||
}
|
||||
@@ -58,49 +51,74 @@ class Settings
|
||||
* If `true` (`mbstring` extension must be enabled), will use (slower) `mb_strlen`, `mb_convert_case`, `mb_substr`
|
||||
* and `mb_strpos` functions. Otherwise, the normal (ASCII-Only) functions will be used.
|
||||
*
|
||||
* @param bool $bMultibyteSupport
|
||||
*
|
||||
* @return self fluent interface
|
||||
* @return $this fluent interface
|
||||
*/
|
||||
public function withMultibyteSupport($bMultibyteSupport = true)
|
||||
public function withMultibyteSupport(bool $multibyteSupport = true): self
|
||||
{
|
||||
$this->bMultibyteSupport = $bMultibyteSupport;
|
||||
$this->multibyteSupport = $multibyteSupport;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the charset to be used if the CSS does not contain an `@charset` declaration.
|
||||
*
|
||||
* @param string $sDefaultCharset
|
||||
* @param non-empty-string $defaultCharset
|
||||
*
|
||||
* @return self fluent interface
|
||||
* @return $this fluent interface
|
||||
*/
|
||||
public function withDefaultCharset($sDefaultCharset)
|
||||
public function withDefaultCharset(string $defaultCharset): self
|
||||
{
|
||||
$this->sDefaultCharset = $sDefaultCharset;
|
||||
$this->defaultCharset = $defaultCharset;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures whether the parser should silently ignore invalid rules.
|
||||
*
|
||||
* @param bool $bLenientParsing
|
||||
*
|
||||
* @return self fluent interface
|
||||
* @return $this fluent interface
|
||||
*/
|
||||
public function withLenientParsing($bLenientParsing = true)
|
||||
public function withLenientParsing(bool $usesLenientParsing = true): self
|
||||
{
|
||||
$this->bLenientParsing = $bLenientParsing;
|
||||
$this->lenientParsing = $usesLenientParsing;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the parser to choke on invalid rules.
|
||||
*
|
||||
* @return self fluent interface
|
||||
* @return $this fluent interface
|
||||
*/
|
||||
public function beStrict()
|
||||
public function beStrict(): self
|
||||
{
|
||||
return $this->withLenientParsing(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function hasMultibyteSupport(): bool
|
||||
{
|
||||
return $this->multibyteSupport;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return non-empty-string
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public function getDefaultCharset(): string
|
||||
{
|
||||
return $this->defaultCharset;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function usesLenientParsing(): bool
|
||||
{
|
||||
return $this->lenientParsing;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sabberworm\CSS;
|
||||
|
||||
/**
|
||||
* Provides a method to obtain the short name of the instantiated class (i.e. without namespace prefix).
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
trait ShortClassNameProvider
|
||||
{
|
||||
/**
|
||||
* @return non-empty-string
|
||||
*/
|
||||
private function getShortClassName(): string
|
||||
{
|
||||
return (new \ReflectionClass($this))->getShortName();
|
||||
}
|
||||
}
|
||||
+73
-50
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sabberworm\CSS\Value;
|
||||
|
||||
use Sabberworm\CSS\OutputFormat;
|
||||
@@ -15,95 +17,116 @@ use Sabberworm\CSS\Parsing\UnexpectedTokenException;
|
||||
class CSSFunction extends ValueList
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
* @var non-empty-string
|
||||
*
|
||||
* @internal since 8.8.0
|
||||
*/
|
||||
protected $sName;
|
||||
protected $name;
|
||||
|
||||
/**
|
||||
* @param string $sName
|
||||
* @param RuleValueList|array<int, RuleValueList|CSSFunction|CSSString|LineName|Size|URL|string> $aArguments
|
||||
* @param string $sSeparator
|
||||
* @param int $iLineNo
|
||||
* @param non-empty-string $name
|
||||
* @param RuleValueList|array<Value|string> $arguments
|
||||
* @param non-empty-string $separator
|
||||
* @param int<1, max>|null $lineNumber
|
||||
*/
|
||||
public function __construct($sName, $aArguments, $sSeparator = ',', $iLineNo = 0)
|
||||
public function __construct(string $name, $arguments, string $separator = ',', ?int $lineNumber = null)
|
||||
{
|
||||
if ($aArguments instanceof RuleValueList) {
|
||||
$sSeparator = $aArguments->getListSeparator();
|
||||
$aArguments = $aArguments->getListComponents();
|
||||
if ($arguments instanceof RuleValueList) {
|
||||
$separator = $arguments->getListSeparator();
|
||||
$arguments = $arguments->getListComponents();
|
||||
}
|
||||
$this->sName = $sName;
|
||||
$this->setPosition($iLineNo); // TODO: redundant?
|
||||
parent::__construct($aArguments, $sSeparator, $iLineNo);
|
||||
$this->name = $name;
|
||||
$this->setPosition($lineNumber); // TODO: redundant?
|
||||
parent::__construct($arguments, $separator, $lineNumber);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ParserState $oParserState
|
||||
* @param bool $bIgnoreCase
|
||||
*
|
||||
* @return CSSFunction
|
||||
*
|
||||
* @throws SourceException
|
||||
* @throws UnexpectedEOFException
|
||||
* @throws UnexpectedTokenException
|
||||
*
|
||||
* @internal since V8.8.0
|
||||
*/
|
||||
public static function parse(ParserState $oParserState, $bIgnoreCase = false)
|
||||
public static function parse(ParserState $parserState, bool $ignoreCase = false): CSSFunction
|
||||
{
|
||||
$mResult = $oParserState->parseIdentifier($bIgnoreCase);
|
||||
$oParserState->consume('(');
|
||||
$aArguments = Value::parseValue($oParserState, ['=', ' ', ',']);
|
||||
$mResult = new CSSFunction($mResult, $aArguments, ',', $oParserState->currentLine());
|
||||
$oParserState->consume(')');
|
||||
return $mResult;
|
||||
$name = self::parseName($parserState, $ignoreCase);
|
||||
$parserState->consume('(');
|
||||
$arguments = self::parseArguments($parserState);
|
||||
|
||||
$result = new CSSFunction($name, $arguments, ',', $parserState->currentLine());
|
||||
$parserState->consume(')');
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
* @throws SourceException
|
||||
* @throws UnexpectedEOFException
|
||||
* @throws UnexpectedTokenException
|
||||
*/
|
||||
public function getName()
|
||||
private static function parseName(ParserState $parserState, bool $ignoreCase = false): string
|
||||
{
|
||||
return $this->sName;
|
||||
return $parserState->parseIdentifier($ignoreCase);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $sName
|
||||
* @return Value|string
|
||||
*
|
||||
* @return void
|
||||
* @throws SourceException
|
||||
* @throws UnexpectedEOFException
|
||||
* @throws UnexpectedTokenException
|
||||
*/
|
||||
public function setName($sName)
|
||||
private static function parseArguments(ParserState $parserState)
|
||||
{
|
||||
$this->sName = $sName;
|
||||
return Value::parseValue($parserState, ['=', ' ', ',']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, RuleValueList|CSSFunction|CSSString|LineName|Size|URL|string>
|
||||
* @return non-empty-string
|
||||
*/
|
||||
public function getArguments()
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->aComponents;
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
* @param non-empty-string $name
|
||||
*/
|
||||
public function setName(string $name): void
|
||||
{
|
||||
$this->name = $name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<Value|string>
|
||||
*/
|
||||
public function getArguments(): array
|
||||
{
|
||||
return $this->components;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return non-empty-string
|
||||
*/
|
||||
public function render(OutputFormat $outputFormat): string
|
||||
{
|
||||
$arguments = parent::render($outputFormat);
|
||||
return "{$this->name}({$arguments})";
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, bool|int|float|string|array<mixed>|null>
|
||||
*
|
||||
* @deprecated in V8.8.0, will be removed in V9.0.0. Use `render` instead.
|
||||
* @internal
|
||||
*/
|
||||
public function __toString()
|
||||
public function getArrayRepresentation(): array
|
||||
{
|
||||
return $this->render(new OutputFormat());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param OutputFormat|null $oOutputFormat
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function render($oOutputFormat)
|
||||
{
|
||||
$aArguments = parent::render($oOutputFormat);
|
||||
return "{$this->sName}({$aArguments})";
|
||||
return \array_merge(
|
||||
[
|
||||
'class' => 'placeholder',
|
||||
'name' => $this->name,
|
||||
],
|
||||
parent::getArrayRepresentation()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+67
-59
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sabberworm\CSS\Value;
|
||||
|
||||
use Sabberworm\CSS\OutputFormat;
|
||||
@@ -7,6 +9,9 @@ use Sabberworm\CSS\Parsing\ParserState;
|
||||
use Sabberworm\CSS\Parsing\SourceException;
|
||||
use Sabberworm\CSS\Parsing\UnexpectedEOFException;
|
||||
use Sabberworm\CSS\Parsing\UnexpectedTokenException;
|
||||
use Sabberworm\CSS\ShortClassNameProvider;
|
||||
|
||||
use function Safe\preg_match;
|
||||
|
||||
/**
|
||||
* This class is a wrapper for quoted strings to distinguish them from keywords.
|
||||
@@ -15,102 +20,105 @@ use Sabberworm\CSS\Parsing\UnexpectedTokenException;
|
||||
*/
|
||||
class CSSString extends PrimitiveValue
|
||||
{
|
||||
use ShortClassNameProvider;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $sString;
|
||||
private $string;
|
||||
|
||||
/**
|
||||
* @param string $sString
|
||||
* @param int $iLineNo
|
||||
* @param int<1, max>|null $lineNumber
|
||||
*/
|
||||
public function __construct($sString, $iLineNo = 0)
|
||||
public function __construct(string $string, ?int $lineNumber = null)
|
||||
{
|
||||
$this->sString = $sString;
|
||||
parent::__construct($iLineNo);
|
||||
$this->string = $string;
|
||||
parent::__construct($lineNumber);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return CSSString
|
||||
*
|
||||
* @throws SourceException
|
||||
* @throws UnexpectedEOFException
|
||||
* @throws UnexpectedTokenException
|
||||
*
|
||||
* @internal since V8.8.0
|
||||
*/
|
||||
public static function parse(ParserState $oParserState)
|
||||
public static function parse(ParserState $parserState): CSSString
|
||||
{
|
||||
$sBegin = $oParserState->peek();
|
||||
$sQuote = null;
|
||||
if ($sBegin === "'") {
|
||||
$sQuote = "'";
|
||||
} elseif ($sBegin === '"') {
|
||||
$sQuote = '"';
|
||||
$begin = $parserState->peek();
|
||||
$quote = null;
|
||||
if ($begin === "'") {
|
||||
$quote = "'";
|
||||
} elseif ($begin === '"') {
|
||||
$quote = '"';
|
||||
}
|
||||
if ($sQuote !== null) {
|
||||
$oParserState->consume($sQuote);
|
||||
if ($quote !== null) {
|
||||
$parserState->consume($quote);
|
||||
}
|
||||
$sResult = "";
|
||||
$sContent = null;
|
||||
if ($sQuote === null) {
|
||||
$result = '';
|
||||
if ($quote === null) {
|
||||
// Unquoted strings end in whitespace or with braces, brackets, parentheses
|
||||
while (!preg_match('/[\\s{}()<>\\[\\]]/isu', $oParserState->peek())) {
|
||||
$sResult .= $oParserState->parseCharacter(false);
|
||||
while (preg_match('/[\\s{}()<>\\[\\]]/isu', $parserState->peek()) === 0) {
|
||||
$result .= $parserState->parseCharacter(false);
|
||||
}
|
||||
} else {
|
||||
while (!$oParserState->comes($sQuote)) {
|
||||
$sContent = $oParserState->parseCharacter(false);
|
||||
if ($sContent === null) {
|
||||
while (!$parserState->comes($quote)) {
|
||||
$content = $parserState->parseCharacter(false);
|
||||
if ($content === null) {
|
||||
throw new SourceException(
|
||||
"Non-well-formed quoted string {$oParserState->peek(3)}",
|
||||
$oParserState->currentLine()
|
||||
"Non-well-formed quoted string {$parserState->peek(3)}",
|
||||
$parserState->currentLine()
|
||||
);
|
||||
}
|
||||
$sResult .= $sContent;
|
||||
$result .= $content;
|
||||
}
|
||||
$oParserState->consume($sQuote);
|
||||
$parserState->consume($quote);
|
||||
}
|
||||
return new CSSString($sResult, $oParserState->currentLine());
|
||||
return new CSSString($result, $parserState->currentLine());
|
||||
}
|
||||
|
||||
public function setString(string $string): void
|
||||
{
|
||||
$this->string = $string;
|
||||
}
|
||||
|
||||
public function getString(): string
|
||||
{
|
||||
return $this->string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $sString
|
||||
* @return non-empty-string
|
||||
*/
|
||||
public function render(OutputFormat $outputFormat): string
|
||||
{
|
||||
return $outputFormat->getStringQuotingType()
|
||||
. $this->escape($this->string, $outputFormat)
|
||||
. $outputFormat->getStringQuotingType();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, bool|int|float|string|array<mixed>|null>
|
||||
*
|
||||
* @return void
|
||||
* @internal
|
||||
*/
|
||||
public function setString($sString)
|
||||
public function getArrayRepresentation(): array
|
||||
{
|
||||
$this->sString = $sString;
|
||||
return [
|
||||
'class' => $this->getShortClassName(),
|
||||
// We're using the term "contents" here to make the difference to the class more clear.
|
||||
'contents' => $this->string,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getString()
|
||||
private function escape(string $string, OutputFormat $outputFormat): string
|
||||
{
|
||||
return $this->sString;
|
||||
}
|
||||
$charactersToEscape = '\\';
|
||||
$charactersToEscape .= ($outputFormat->getStringQuotingType() === '"' ? '"' : "'");
|
||||
$withEscapedQuotes = \addcslashes($string, $charactersToEscape);
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*
|
||||
* @deprecated in V8.8.0, will be removed in V9.0.0. Use `render` instead.
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
return $this->render(new OutputFormat());
|
||||
}
|
||||
$withNewlineEncoded = \str_replace("\n", '\\A', $withEscapedQuotes);
|
||||
|
||||
/**
|
||||
* @param OutputFormat|null $oOutputFormat
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function render($oOutputFormat)
|
||||
{
|
||||
$sString = addslashes($this->sString);
|
||||
$sString = str_replace("\n", '\A', $sString);
|
||||
return $oOutputFormat->getStringQuotingType() . $sString . $oOutputFormat->getStringQuotingType();
|
||||
return $withNewlineEncoded;
|
||||
}
|
||||
}
|
||||
|
||||
+61
-67
@@ -1,115 +1,109 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sabberworm\CSS\Value;
|
||||
|
||||
use Sabberworm\CSS\Parsing\ParserState;
|
||||
use Sabberworm\CSS\Parsing\UnexpectedEOFException;
|
||||
use Sabberworm\CSS\Parsing\UnexpectedTokenException;
|
||||
|
||||
/**
|
||||
* Support for `-webkit-calc` and `-moz-calc` is deprecated in version 8.8.0, and will be removed in version 9.0.0.
|
||||
*/
|
||||
use function Safe\preg_match;
|
||||
|
||||
class CalcFunction extends CSSFunction
|
||||
{
|
||||
/**
|
||||
* @var int
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
const T_OPERAND = 1;
|
||||
private const T_OPERAND = 1;
|
||||
private const T_OPERATOR = 2;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
const T_OPERATOR = 2;
|
||||
|
||||
/**
|
||||
* @param ParserState $oParserState
|
||||
* @param bool $bIgnoreCase
|
||||
*
|
||||
* @return CalcFunction
|
||||
*
|
||||
* @throws UnexpectedTokenException
|
||||
* @throws UnexpectedEOFException
|
||||
*
|
||||
* @internal since V8.8.0
|
||||
*/
|
||||
public static function parse(ParserState $oParserState, $bIgnoreCase = false)
|
||||
public static function parse(ParserState $parserState, bool $ignoreCase = false): CSSFunction
|
||||
{
|
||||
$aOperators = ['+', '-', '*', '/'];
|
||||
$sFunction = $oParserState->parseIdentifier();
|
||||
if ($oParserState->peek() != '(') {
|
||||
$operators = ['+', '-', '*', '/'];
|
||||
$function = $parserState->parseIdentifier();
|
||||
if ($parserState->peek() !== '(') {
|
||||
// Found ; or end of line before an opening bracket
|
||||
throw new UnexpectedTokenException('(', $oParserState->peek(), 'literal', $oParserState->currentLine());
|
||||
} elseif (!in_array($sFunction, ['calc', '-moz-calc', '-webkit-calc'])) {
|
||||
throw new UnexpectedTokenException('(', $parserState->peek(), 'literal', $parserState->currentLine());
|
||||
} elseif ($function !== 'calc') {
|
||||
// Found invalid calc definition. Example calc (...
|
||||
throw new UnexpectedTokenException('calc', $sFunction, 'literal', $oParserState->currentLine());
|
||||
throw new UnexpectedTokenException('calc', $function, 'literal', $parserState->currentLine());
|
||||
}
|
||||
$oParserState->consume('(');
|
||||
$oCalcList = new CalcRuleValueList($oParserState->currentLine());
|
||||
$oList = new RuleValueList(',', $oParserState->currentLine());
|
||||
$iNestingLevel = 0;
|
||||
$iLastComponentType = null;
|
||||
while (!$oParserState->comes(')') || $iNestingLevel > 0) {
|
||||
if ($oParserState->isEnd() && $iNestingLevel === 0) {
|
||||
$parserState->consume('(');
|
||||
$calcRuleValueList = new CalcRuleValueList($parserState->currentLine());
|
||||
$list = new RuleValueList(',', $parserState->currentLine());
|
||||
$nestingLevel = 0;
|
||||
$lastComponentType = null;
|
||||
while (!$parserState->comes(')') || $nestingLevel > 0) {
|
||||
if ($parserState->isEnd() && $nestingLevel === 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
$oParserState->consumeWhiteSpace();
|
||||
if ($oParserState->comes('(')) {
|
||||
$iNestingLevel++;
|
||||
$oCalcList->addListComponent($oParserState->consume(1));
|
||||
$oParserState->consumeWhiteSpace();
|
||||
$parserState->consumeWhiteSpace();
|
||||
if ($parserState->comes('(')) {
|
||||
$nestingLevel++;
|
||||
$calcRuleValueList->addListComponent($parserState->consume(1));
|
||||
$parserState->consumeWhiteSpace();
|
||||
continue;
|
||||
} elseif ($oParserState->comes(')')) {
|
||||
$iNestingLevel--;
|
||||
$oCalcList->addListComponent($oParserState->consume(1));
|
||||
$oParserState->consumeWhiteSpace();
|
||||
} elseif ($parserState->comes(')')) {
|
||||
$nestingLevel--;
|
||||
$calcRuleValueList->addListComponent($parserState->consume(1));
|
||||
$parserState->consumeWhiteSpace();
|
||||
continue;
|
||||
}
|
||||
if ($iLastComponentType != CalcFunction::T_OPERAND) {
|
||||
$oVal = Value::parsePrimitiveValue($oParserState);
|
||||
$oCalcList->addListComponent($oVal);
|
||||
$iLastComponentType = CalcFunction::T_OPERAND;
|
||||
if ($lastComponentType !== CalcFunction::T_OPERAND) {
|
||||
$value = Value::parsePrimitiveValue($parserState);
|
||||
$calcRuleValueList->addListComponent($value);
|
||||
$lastComponentType = CalcFunction::T_OPERAND;
|
||||
} else {
|
||||
if (in_array($oParserState->peek(), $aOperators)) {
|
||||
if (($oParserState->comes('-') || $oParserState->comes('+'))) {
|
||||
if (\in_array($parserState->peek(), $operators, true)) {
|
||||
if (($parserState->comes('-') || $parserState->comes('+'))) {
|
||||
if (
|
||||
$oParserState->peek(1, -1) != ' '
|
||||
|| !($oParserState->comes('- ')
|
||||
|| $oParserState->comes('+ '))
|
||||
preg_match('/\\s/', $parserState->peek(1, -1)) !== 1
|
||||
|| preg_match('/\\s/', $parserState->peek(1, 1)) !== 1
|
||||
) {
|
||||
throw new UnexpectedTokenException(
|
||||
" {$oParserState->peek()} ",
|
||||
$oParserState->peek(1, -1) . $oParserState->peek(2),
|
||||
" {$parserState->peek()} ",
|
||||
$parserState->peek(1, -1) . $parserState->peek(2),
|
||||
'literal',
|
||||
$oParserState->currentLine()
|
||||
$parserState->currentLine()
|
||||
);
|
||||
}
|
||||
}
|
||||
$oCalcList->addListComponent($oParserState->consume(1));
|
||||
$iLastComponentType = CalcFunction::T_OPERATOR;
|
||||
$calcRuleValueList->addListComponent($parserState->consume(1));
|
||||
$lastComponentType = CalcFunction::T_OPERATOR;
|
||||
} else {
|
||||
throw new UnexpectedTokenException(
|
||||
sprintf(
|
||||
\sprintf(
|
||||
'Next token was expected to be an operand of type %s. Instead "%s" was found.',
|
||||
implode(', ', $aOperators),
|
||||
$oParserState->peek()
|
||||
\implode(', ', $operators),
|
||||
$parserState->peek()
|
||||
),
|
||||
'',
|
||||
'custom',
|
||||
$oParserState->currentLine()
|
||||
$parserState->currentLine()
|
||||
);
|
||||
}
|
||||
}
|
||||
$oParserState->consumeWhiteSpace();
|
||||
$parserState->consumeWhiteSpace();
|
||||
}
|
||||
$oList->addListComponent($oCalcList);
|
||||
if (!$oParserState->isEnd()) {
|
||||
$oParserState->consume(')');
|
||||
$list->addListComponent($calcRuleValueList);
|
||||
if (!$parserState->isEnd()) {
|
||||
$parserState->consume(')');
|
||||
}
|
||||
return new CalcFunction($sFunction, $oList, ',', $oParserState->currentLine());
|
||||
return new CalcFunction($function, $list, ',', $parserState->currentLine());
|
||||
}
|
||||
|
||||
/**
|
||||
* @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 . '`');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sabberworm\CSS\Value;
|
||||
|
||||
use Sabberworm\CSS\OutputFormat;
|
||||
@@ -7,20 +9,25 @@ use Sabberworm\CSS\OutputFormat;
|
||||
class CalcRuleValueList extends RuleValueList
|
||||
{
|
||||
/**
|
||||
* @param int $iLineNo
|
||||
* @param int<1, max>|null $lineNumber
|
||||
*/
|
||||
public function __construct($iLineNo = 0)
|
||||
public function __construct(?int $lineNumber = null)
|
||||
{
|
||||
parent::__construct(',', $iLineNo);
|
||||
parent::__construct(',', $lineNumber);
|
||||
}
|
||||
|
||||
public function render(OutputFormat $outputFormat): string
|
||||
{
|
||||
return $outputFormat->getFormatter()->implode(' ', $this->components);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param OutputFormat|null $oOutputFormat
|
||||
* @return array<string, bool|int|float|string|array<mixed>|null>
|
||||
*
|
||||
* @return string
|
||||
* @internal
|
||||
*/
|
||||
public function render($oOutputFormat)
|
||||
public function getArrayRepresentation(): array
|
||||
{
|
||||
return $oOutputFormat->implode(' ', $this->aComponents);
|
||||
throw new \BadMethodCallException('`getArrayRepresentation` is not yet implemented for `' . self::class . '`');
|
||||
}
|
||||
}
|
||||
|
||||
+331
-117
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sabberworm\CSS\Value;
|
||||
|
||||
use Sabberworm\CSS\OutputFormat;
|
||||
@@ -14,173 +16,385 @@ use Sabberworm\CSS\Parsing\UnexpectedTokenException;
|
||||
class Color extends CSSFunction
|
||||
{
|
||||
/**
|
||||
* @param array<int, RuleValueList|CSSFunction|CSSString|LineName|Size|URL|string> $aColor
|
||||
* @param int $iLineNo
|
||||
* @param array<non-empty-string, Value|string> $colorValues
|
||||
* @param int<1, max>|null $lineNumber
|
||||
*/
|
||||
public function __construct(array $aColor, $iLineNo = 0)
|
||||
public function __construct(array $colorValues, ?int $lineNumber = null)
|
||||
{
|
||||
parent::__construct(implode('', array_keys($aColor)), $aColor, ',', $iLineNo);
|
||||
parent::__construct(\implode('', \array_keys($colorValues)), $colorValues, ',', $lineNumber);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ParserState $oParserState
|
||||
* @param bool $bIgnoreCase
|
||||
*
|
||||
* @return Color|CSSFunction
|
||||
*
|
||||
* @throws UnexpectedEOFException
|
||||
* @throws UnexpectedTokenException
|
||||
*
|
||||
* @internal since V8.8.0
|
||||
*/
|
||||
public static function parse(ParserState $oParserState, $bIgnoreCase = false)
|
||||
public static function parse(ParserState $parserState, bool $ignoreCase = false): CSSFunction
|
||||
{
|
||||
$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];
|
||||
}
|
||||
return $parserState->comes('#')
|
||||
? self::parseHexColor($parserState)
|
||||
: self::parseColorFunction($parserState);
|
||||
}
|
||||
|
||||
if ($oParserState->strlen($sValue) === 8) {
|
||||
$aColor = [
|
||||
'r' => new Size(intval($sValue[0] . $sValue[1], 16), null, true, $oParserState->currentLine()),
|
||||
'g' => new Size(intval($sValue[2] . $sValue[3], 16), null, true, $oParserState->currentLine()),
|
||||
'b' => new Size(intval($sValue[4] . $sValue[5], 16), null, true, $oParserState->currentLine()),
|
||||
'a' => new Size(
|
||||
round(self::mapRange(intval($sValue[6] . $sValue[7], 16), 0, 255, 0, 1), 2),
|
||||
null,
|
||||
true,
|
||||
$oParserState->currentLine()
|
||||
),
|
||||
];
|
||||
} elseif ($oParserState->strlen($sValue) === 6) {
|
||||
$aColor = [
|
||||
'r' => new Size(intval($sValue[0] . $sValue[1], 16), null, true, $oParserState->currentLine()),
|
||||
'g' => new Size(intval($sValue[2] . $sValue[3], 16), null, true, $oParserState->currentLine()),
|
||||
'b' => new Size(intval($sValue[4] . $sValue[5], 16), null, true, $oParserState->currentLine()),
|
||||
];
|
||||
} else {
|
||||
throw new UnexpectedTokenException(
|
||||
'Invalid hex color value',
|
||||
$sValue,
|
||||
'custom',
|
||||
$oParserState->currentLine()
|
||||
);
|
||||
}
|
||||
/**
|
||||
* @throws UnexpectedEOFException
|
||||
* @throws UnexpectedTokenException
|
||||
*/
|
||||
private static function parseHexColor(ParserState $parserState): Color
|
||||
{
|
||||
$parserState->consume('#');
|
||||
$hexValue = $parserState->parseIdentifier(false);
|
||||
if ($parserState->strlen($hexValue) === 3) {
|
||||
$hexValue = $hexValue[0] . $hexValue[0] . $hexValue[1] . $hexValue[1] . $hexValue[2] . $hexValue[2];
|
||||
} elseif ($parserState->strlen($hexValue) === 4) {
|
||||
$hexValue = $hexValue[0] . $hexValue[0] . $hexValue[1] . $hexValue[1] . $hexValue[2] . $hexValue[2]
|
||||
. $hexValue[3] . $hexValue[3];
|
||||
}
|
||||
|
||||
if ($parserState->strlen($hexValue) === 8) {
|
||||
$colorValues = [
|
||||
'r' => new Size(\intval($hexValue[0] . $hexValue[1], 16), null, true, $parserState->currentLine()),
|
||||
'g' => new Size(\intval($hexValue[2] . $hexValue[3], 16), null, true, $parserState->currentLine()),
|
||||
'b' => new Size(\intval($hexValue[4] . $hexValue[5], 16), null, true, $parserState->currentLine()),
|
||||
'a' => new Size(
|
||||
\round(self::mapRange(\intval($hexValue[6] . $hexValue[7], 16), 0, 255, 0, 1), 2),
|
||||
null,
|
||||
true,
|
||||
$parserState->currentLine()
|
||||
),
|
||||
];
|
||||
} elseif ($parserState->strlen($hexValue) === 6) {
|
||||
$colorValues = [
|
||||
'r' => new Size(\intval($hexValue[0] . $hexValue[1], 16), null, true, $parserState->currentLine()),
|
||||
'g' => new Size(\intval($hexValue[2] . $hexValue[3], 16), null, true, $parserState->currentLine()),
|
||||
'b' => new Size(\intval($hexValue[4] . $hexValue[5], 16), null, true, $parserState->currentLine()),
|
||||
];
|
||||
} else {
|
||||
$sColorMode = $oParserState->parseIdentifier(true);
|
||||
$oParserState->consumeWhiteSpace();
|
||||
$oParserState->consume('(');
|
||||
throw new UnexpectedTokenException(
|
||||
'Invalid hex color value',
|
||||
$hexValue,
|
||||
'custom',
|
||||
$parserState->currentLine()
|
||||
);
|
||||
}
|
||||
|
||||
$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);
|
||||
}
|
||||
return new Color($colorValues, $parserState->currentLine());
|
||||
}
|
||||
|
||||
if ($bContainsVar && $oParserState->comes(')')) {
|
||||
// With a var argument the function can have fewer arguments
|
||||
break;
|
||||
}
|
||||
/**
|
||||
* @throws UnexpectedEOFException
|
||||
* @throws UnexpectedTokenException
|
||||
*/
|
||||
private static function parseColorFunction(ParserState $parserState): CSSFunction
|
||||
{
|
||||
$colorValues = [];
|
||||
|
||||
$oParserState->consumeWhiteSpace();
|
||||
if ($i < ($iLength - 1)) {
|
||||
$oParserState->consume(',');
|
||||
}
|
||||
$colorMode = $parserState->parseIdentifier(true);
|
||||
$parserState->consumeWhiteSpace();
|
||||
$parserState->consume('(');
|
||||
|
||||
// CSS Color Module Level 4 says that `rgb` and `rgba` are now aliases; likewise `hsl` and `hsla`.
|
||||
// So, attempt to parse with the `a`, and allow for it not being there.
|
||||
switch ($colorMode) {
|
||||
case 'rgb':
|
||||
$colorModeForParsing = 'rgba';
|
||||
$mayHaveOptionalAlpha = true;
|
||||
break;
|
||||
case 'hsl':
|
||||
$colorModeForParsing = 'hsla';
|
||||
$mayHaveOptionalAlpha = true;
|
||||
break;
|
||||
case 'rgba':
|
||||
// This is handled identically to the following case.
|
||||
case 'hsla':
|
||||
$colorModeForParsing = $colorMode;
|
||||
$mayHaveOptionalAlpha = true;
|
||||
break;
|
||||
default:
|
||||
$colorModeForParsing = $colorMode;
|
||||
$mayHaveOptionalAlpha = false;
|
||||
}
|
||||
|
||||
$containsVar = false;
|
||||
$containsNone = false;
|
||||
$isLegacySyntax = false;
|
||||
$expectedArgumentCount = $parserState->strlen($colorModeForParsing);
|
||||
for ($argumentIndex = 0; $argumentIndex < $expectedArgumentCount; ++$argumentIndex) {
|
||||
$parserState->consumeWhiteSpace();
|
||||
$valueKey = $colorModeForParsing[$argumentIndex];
|
||||
if ($parserState->comes('var')) {
|
||||
$colorValues[$valueKey] = CSSFunction::parseIdentifierOrFunction($parserState);
|
||||
$containsVar = true;
|
||||
} elseif (!$isLegacySyntax && $parserState->comes('none')) {
|
||||
$colorValues[$valueKey] = $parserState->parseIdentifier();
|
||||
$containsNone = true;
|
||||
} else {
|
||||
$colorValues[$valueKey] = Size::parse($parserState, true);
|
||||
}
|
||||
$oParserState->consume(')');
|
||||
|
||||
if ($bContainsVar) {
|
||||
return new CSSFunction($sColorMode, array_values($aColor), ',', $oParserState->currentLine());
|
||||
// This must be done first, to consume comments as well, so that the `comes` test will work.
|
||||
$parserState->consumeWhiteSpace();
|
||||
|
||||
// With a `var` argument, the function can have fewer arguments.
|
||||
// And as of CSS Color Module Level 4, the alpha argument is optional.
|
||||
$canCloseNow
|
||||
= $containsVar
|
||||
|| ($mayHaveOptionalAlpha && $argumentIndex >= $expectedArgumentCount - 2);
|
||||
if ($canCloseNow && $parserState->comes(')')) {
|
||||
break;
|
||||
}
|
||||
|
||||
// "Legacy" syntax is comma-delimited, and does not allow the `none` keyword.
|
||||
// "Modern" syntax is space-delimited, with `/` as alpha delimiter.
|
||||
// They cannot be mixed.
|
||||
if ($argumentIndex === 0 && !$containsNone) {
|
||||
// An immediate closing parenthesis is not valid.
|
||||
if ($parserState->comes(')')) {
|
||||
throw new UnexpectedTokenException(
|
||||
'Color function with no arguments',
|
||||
'',
|
||||
'custom',
|
||||
$parserState->currentLine()
|
||||
);
|
||||
}
|
||||
$isLegacySyntax = $parserState->comes(',');
|
||||
}
|
||||
|
||||
if ($isLegacySyntax && $argumentIndex < ($expectedArgumentCount - 1)) {
|
||||
$parserState->consume(',');
|
||||
}
|
||||
|
||||
// In the "modern" syntax, the alpha value must be delimited with `/`.
|
||||
if (!$isLegacySyntax) {
|
||||
if ($containsVar) {
|
||||
// If the `var` substitution encompasses more than one argument,
|
||||
// the alpha deliminator may come at any time.
|
||||
if ($parserState->comes('/')) {
|
||||
$parserState->consume('/');
|
||||
}
|
||||
} elseif (($colorModeForParsing[$argumentIndex + 1] ?? '') === 'a') {
|
||||
// Alpha value is the next expected argument.
|
||||
// Since a closing parenthesis was not found, a `/` separator is now required.
|
||||
$parserState->consume('/');
|
||||
}
|
||||
}
|
||||
}
|
||||
return new Color($aColor, $oParserState->currentLine());
|
||||
$parserState->consume(')');
|
||||
|
||||
return $containsVar
|
||||
? new CSSFunction($colorMode, \array_values($colorValues), ',', $parserState->currentLine())
|
||||
: new Color($colorValues, $parserState->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)
|
||||
private static function mapRange(float $value, float $fromMin, float $fromMax, float $toMin, float $toMax): float
|
||||
{
|
||||
$fFromRange = $fFromMax - $fFromMin;
|
||||
$fToRange = $fToMax - $fToMin;
|
||||
$fMultiplier = $fToRange / $fFromRange;
|
||||
$fNewVal = $fVal - $fFromMin;
|
||||
$fNewVal *= $fMultiplier;
|
||||
return $fNewVal + $fToMin;
|
||||
$fromRange = $fromMax - $fromMin;
|
||||
$toRange = $toMax - $toMin;
|
||||
$multiplier = $toRange / $fromRange;
|
||||
$newValue = $value - $fromMin;
|
||||
$newValue *= $multiplier;
|
||||
|
||||
return $newValue + $toMin;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, RuleValueList|CSSFunction|CSSString|LineName|Size|URL|string>
|
||||
* @return array<non-empty-string, Value|string>
|
||||
*/
|
||||
public function getColor()
|
||||
public function getColor(): array
|
||||
{
|
||||
return $this->aComponents;
|
||||
return $this->components;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, RuleValueList|CSSFunction|CSSString|LineName|Size|URL|string> $aColor
|
||||
*
|
||||
* @return void
|
||||
* @param array<non-empty-string, Value|string> $colorValues
|
||||
*/
|
||||
public function setColor(array $aColor)
|
||||
public function setColor(array $colorValues): void
|
||||
{
|
||||
$this->setName(implode('', array_keys($aColor)));
|
||||
$this->aComponents = $aColor;
|
||||
$this->setName(\implode('', \array_keys($colorValues)));
|
||||
$this->components = $colorValues;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
* @return non-empty-string
|
||||
*/
|
||||
public function getColorDescription()
|
||||
public function getColorDescription(): string
|
||||
{
|
||||
return $this->getName();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*
|
||||
* @deprecated in V8.8.0, will be removed in V9.0.0. Use `render` instead.
|
||||
* @return non-empty-string
|
||||
*/
|
||||
public function __toString()
|
||||
public function render(OutputFormat $outputFormat): string
|
||||
{
|
||||
return $this->render(new OutputFormat());
|
||||
if ($this->shouldRenderAsHex($outputFormat)) {
|
||||
return $this->renderAsHex();
|
||||
}
|
||||
|
||||
if ($this->shouldRenderInModernSyntax()) {
|
||||
return $this->renderInModernSyntax($outputFormat);
|
||||
}
|
||||
|
||||
return parent::render($outputFormat);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param OutputFormat|null $oOutputFormat
|
||||
* @return array<string, bool|int|float|string|array<mixed>|null>
|
||||
*
|
||||
* @return string
|
||||
* @internal
|
||||
*/
|
||||
public function render($oOutputFormat)
|
||||
public function getArrayRepresentation(): array
|
||||
{
|
||||
// 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);
|
||||
throw new \BadMethodCallException('`getArrayRepresentation` is not yet implemented for `' . self::class . '`');
|
||||
}
|
||||
|
||||
private function shouldRenderAsHex(OutputFormat $outputFormat): bool
|
||||
{
|
||||
return
|
||||
$outputFormat->usesRgbHashNotation()
|
||||
&& $this->getRealName() === 'rgb'
|
||||
&& $this->allComponentsAreNumbers();
|
||||
}
|
||||
|
||||
/**
|
||||
* The function name is a concatenation of the array keys of the components, which is passed to the constructor.
|
||||
* However, this can be changed by calling {@see CSSFunction::setName},
|
||||
* so is not reliable in situations where it's necessary to determine the function name based on the components.
|
||||
*/
|
||||
private function getRealName(): string
|
||||
{
|
||||
return \implode('', \array_keys($this->components));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test whether all color components are absolute numbers (CSS type `number`), not percentages or anything else.
|
||||
* If any component is not an instance of `Size`, the method will also return `false`.
|
||||
*/
|
||||
private function allComponentsAreNumbers(): bool
|
||||
{
|
||||
foreach ($this->components as $component) {
|
||||
if (!($component instanceof Size) || $component->getUnit() !== null) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return parent::render($oOutputFormat);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Note that this method assumes the following:
|
||||
* - The `components` array has keys for `r`, `g` and `b`;
|
||||
* - The values in the array are all instances of `Size`.
|
||||
*
|
||||
* Errors will be triggered or thrown if this is not the case.
|
||||
*
|
||||
* @return non-empty-string
|
||||
*/
|
||||
private function renderAsHex(): string
|
||||
{
|
||||
$result = \sprintf(
|
||||
'%02x%02x%02x',
|
||||
$this->components['r']->getSize(),
|
||||
$this->components['g']->getSize(),
|
||||
$this->components['b']->getSize()
|
||||
);
|
||||
$canUseShortVariant = ($result[0] === $result[1]) && ($result[2] === $result[3]) && ($result[4] === $result[5]);
|
||||
|
||||
return '#' . ($canUseShortVariant ? $result[0] . $result[2] . $result[4] : $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* The "legacy" syntax does not allow RGB colors to have a mixture of `percentage`s and `number`s,
|
||||
* and does not allow `none` as any component value.
|
||||
*
|
||||
* The "legacy" and "modern" monikers are part of the formal W3C syntax.
|
||||
* See the following for more information:
|
||||
* - {@link
|
||||
* https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/rgb#formal_syntax
|
||||
* Description of the formal syntax for `rgb()` on MDN
|
||||
* };
|
||||
* - {@link
|
||||
* https://www.w3.org/TR/css-color-4/#rgb-functions
|
||||
* The same in the CSS Color Module Level 4 W3C Candidate Recommendation Draft
|
||||
* } (as of 13 February 2024, at time of writing).
|
||||
*/
|
||||
private function shouldRenderInModernSyntax(): bool
|
||||
{
|
||||
if ($this->hasNoneAsComponentValue()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!$this->colorFunctionMayHaveMixedValueTypes($this->getRealName())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$hasPercentage = false;
|
||||
$hasNumber = false;
|
||||
foreach ($this->components as $key => $value) {
|
||||
if ($key === 'a') {
|
||||
// Alpha can have units that don't match those of the RGB components in the "legacy" syntax.
|
||||
// So it is not necessary to check it. It's also always last, hence `break` rather than `continue`.
|
||||
break;
|
||||
}
|
||||
if (!($value instanceof Size)) {
|
||||
// Unexpected, unknown, or modified via the API
|
||||
return false;
|
||||
}
|
||||
$unit = $value->getUnit();
|
||||
// `switch` only does loose comparison
|
||||
if ($unit === null) {
|
||||
$hasNumber = true;
|
||||
} elseif ($unit === '%') {
|
||||
$hasPercentage = true;
|
||||
} else {
|
||||
// Invalid unit
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return $hasPercentage && $hasNumber;
|
||||
}
|
||||
|
||||
private function hasNoneAsComponentValue(): bool
|
||||
{
|
||||
return \in_array('none', $this->components, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Some color functions, such as `rgb`,
|
||||
* may have a mixture of `percentage`, `number`, or possibly other types in their arguments.
|
||||
*
|
||||
* Note that this excludes the alpha component, which is treated separately.
|
||||
*/
|
||||
private function colorFunctionMayHaveMixedValueTypes(string $function): bool
|
||||
{
|
||||
$functionsThatMayHaveMixedValueTypes = ['rgb', 'rgba'];
|
||||
|
||||
return \in_array($function, $functionsThatMayHaveMixedValueTypes, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return non-empty-string
|
||||
*/
|
||||
private function renderInModernSyntax(OutputFormat $outputFormat): string
|
||||
{
|
||||
// Maybe not yet without alpha, but will be...
|
||||
$componentsWithoutAlpha = $this->components;
|
||||
\end($componentsWithoutAlpha);
|
||||
if (\key($componentsWithoutAlpha) === 'a') {
|
||||
$alpha = $this->components['a'];
|
||||
unset($componentsWithoutAlpha['a']);
|
||||
}
|
||||
|
||||
$formatter = $outputFormat->getFormatter();
|
||||
$arguments = $formatter->implode(' ', $componentsWithoutAlpha);
|
||||
if (isset($alpha)) {
|
||||
$separator = $formatter->spaceBeforeListArgumentSeparator('/')
|
||||
. '/' . $formatter->spaceAfterListArgumentSeparator('/');
|
||||
$arguments = $formatter->implode($separator, [$arguments, $alpha]);
|
||||
}
|
||||
|
||||
return $this->getName() . '(' . $arguments . ')';
|
||||
}
|
||||
}
|
||||
|
||||
+26
-32
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sabberworm\CSS\Value;
|
||||
|
||||
use Sabberworm\CSS\OutputFormat;
|
||||
@@ -7,64 +9,56 @@ use Sabberworm\CSS\Parsing\ParserState;
|
||||
use Sabberworm\CSS\Parsing\UnexpectedEOFException;
|
||||
use Sabberworm\CSS\Parsing\UnexpectedTokenException;
|
||||
|
||||
/**
|
||||
* A name for a named CSS grid line.
|
||||
*
|
||||
* @see https://www.w3.org/TR/css-grid-1/#line-name
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Grid_layout/Named_grid_lines
|
||||
*/
|
||||
class LineName extends ValueList
|
||||
{
|
||||
/**
|
||||
* @param array<int, RuleValueList|CSSFunction|CSSString|LineName|Size|URL|string> $aComponents
|
||||
* @param int $iLineNo
|
||||
* @param array<string> $components
|
||||
* @param int<1, max>|null $lineNumber
|
||||
*/
|
||||
public function __construct(array $aComponents = [], $iLineNo = 0)
|
||||
public function __construct(array $components = [], ?int $lineNumber = null)
|
||||
{
|
||||
parent::__construct($aComponents, ' ', $iLineNo);
|
||||
parent::__construct($components, ' ', $lineNumber);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return LineName
|
||||
*
|
||||
* @throws UnexpectedTokenException
|
||||
* @throws UnexpectedEOFException
|
||||
*
|
||||
* @internal since V8.8.0
|
||||
*/
|
||||
public static function parse(ParserState $oParserState)
|
||||
public static function parse(ParserState $parserState): LineName
|
||||
{
|
||||
$oParserState->consume('[');
|
||||
$oParserState->consumeWhiteSpace();
|
||||
$aNames = [];
|
||||
$parserState->consume('[');
|
||||
$parserState->consumeWhiteSpace();
|
||||
$names = [];
|
||||
do {
|
||||
if ($oParserState->getSettings()->bLenientParsing) {
|
||||
if ($parserState->getSettings()->usesLenientParsing()) {
|
||||
try {
|
||||
$aNames[] = $oParserState->parseIdentifier();
|
||||
$names[] = $parserState->parseIdentifier();
|
||||
} catch (UnexpectedTokenException $e) {
|
||||
if (!$oParserState->comes(']')) {
|
||||
if (!$parserState->comes(']')) {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$aNames[] = $oParserState->parseIdentifier();
|
||||
$names[] = $parserState->parseIdentifier();
|
||||
}
|
||||
$oParserState->consumeWhiteSpace();
|
||||
} while (!$oParserState->comes(']'));
|
||||
$oParserState->consume(']');
|
||||
return new LineName($aNames, $oParserState->currentLine());
|
||||
$parserState->consumeWhiteSpace();
|
||||
} while (!$parserState->comes(']'));
|
||||
$parserState->consume(']');
|
||||
return new LineName($names, $parserState->currentLine());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*
|
||||
* @deprecated in V8.8.0, will be removed in V9.0.0. Use `render` instead.
|
||||
* @return non-empty-string
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
return $this->render(new OutputFormat());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param OutputFormat|null $oOutputFormat
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function render($oOutputFormat)
|
||||
public function render(OutputFormat $outputFormat): string
|
||||
{
|
||||
return '[' . parent::render(OutputFormat::createCompact()) . ']';
|
||||
}
|
||||
|
||||
@@ -1,14 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sabberworm\CSS\Value;
|
||||
|
||||
abstract class PrimitiveValue extends Value
|
||||
{
|
||||
/**
|
||||
* @param int $iLineNo
|
||||
*/
|
||||
public function __construct($iLineNo = 0)
|
||||
{
|
||||
parent::__construct($iLineNo);
|
||||
}
|
||||
}
|
||||
abstract class PrimitiveValue extends Value {}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sabberworm\CSS\Value;
|
||||
|
||||
/**
|
||||
@@ -10,11 +12,11 @@ namespace Sabberworm\CSS\Value;
|
||||
class RuleValueList extends ValueList
|
||||
{
|
||||
/**
|
||||
* @param string $sSeparator
|
||||
* @param int $iLineNo
|
||||
* @param non-empty-string $separator
|
||||
* @param int<1, max>|null $lineNumber
|
||||
*/
|
||||
public function __construct($sSeparator = ',', $iLineNo = 0)
|
||||
public function __construct(string $separator = ',', ?int $lineNumber = null)
|
||||
{
|
||||
parent::__construct([], $sSeparator, $iLineNo);
|
||||
parent::__construct([], $separator, $lineNumber);
|
||||
}
|
||||
}
|
||||
|
||||
+91
-115
@@ -1,25 +1,29 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sabberworm\CSS\Value;
|
||||
|
||||
use Sabberworm\CSS\OutputFormat;
|
||||
use Sabberworm\CSS\Parsing\ParserState;
|
||||
use Sabberworm\CSS\Parsing\UnexpectedEOFException;
|
||||
use Sabberworm\CSS\Parsing\UnexpectedTokenException;
|
||||
use Sabberworm\CSS\ShortClassNameProvider;
|
||||
|
||||
use function Safe\preg_match;
|
||||
use function Safe\preg_replace;
|
||||
|
||||
/**
|
||||
* A `Size` consists of a numeric `size` value and a unit.
|
||||
*/
|
||||
class Size extends PrimitiveValue
|
||||
{
|
||||
use ShortClassNameProvider;
|
||||
|
||||
/**
|
||||
* vh/vw/vm(ax)/vmin/rem are absolute insofar as they don’t scale to the immediate parent (only the viewport)
|
||||
*
|
||||
* @var array<int, string>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
const ABSOLUTE_SIZE_UNITS = [
|
||||
private const ABSOLUTE_SIZE_UNITS = [
|
||||
'px',
|
||||
'pt',
|
||||
'pc',
|
||||
@@ -37,211 +41,183 @@ class Size extends PrimitiveValue
|
||||
'rem',
|
||||
];
|
||||
|
||||
/**
|
||||
* @var array<int, string>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
const RELATIVE_SIZE_UNITS = ['%', 'em', 'ex', 'ch', 'fr'];
|
||||
private const RELATIVE_SIZE_UNITS = ['%', 'em', 'ex', 'ch', 'fr'];
|
||||
|
||||
private const NON_SIZE_UNITS = ['deg', 'grad', 'rad', 's', 'ms', 'turn', 'Hz', 'kHz'];
|
||||
|
||||
/**
|
||||
* @var array<int, string>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
const NON_SIZE_UNITS = ['deg', 'grad', 'rad', 's', 'ms', 'turn', 'Hz', 'kHz'];
|
||||
|
||||
/**
|
||||
* @var array<int, array<string, string>>|null
|
||||
* @var array<int<1, max>, array<lowercase-string, non-empty-string>>|null
|
||||
*/
|
||||
private static $SIZE_UNITS = null;
|
||||
|
||||
/**
|
||||
* @var float
|
||||
*/
|
||||
private $fSize;
|
||||
private $size;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
private $sUnit;
|
||||
private $unit;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $bIsColorComponent;
|
||||
private $isColorComponent;
|
||||
|
||||
/**
|
||||
* @param float|int|string $fSize
|
||||
* @param string|null $sUnit
|
||||
* @param bool $bIsColorComponent
|
||||
* @param int $iLineNo
|
||||
* @param float|int|string $size
|
||||
* @param int<1, max>|null $lineNumber
|
||||
*/
|
||||
public function __construct($fSize, $sUnit = null, $bIsColorComponent = false, $iLineNo = 0)
|
||||
public function __construct($size, ?string $unit = null, bool $isColorComponent = false, ?int $lineNumber = null)
|
||||
{
|
||||
parent::__construct($iLineNo);
|
||||
$this->fSize = (float)$fSize;
|
||||
$this->sUnit = $sUnit;
|
||||
$this->bIsColorComponent = $bIsColorComponent;
|
||||
parent::__construct($lineNumber);
|
||||
$this->size = (float) $size;
|
||||
$this->unit = $unit;
|
||||
$this->isColorComponent = $isColorComponent;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $bIsColorComponent
|
||||
*
|
||||
* @return Size
|
||||
*
|
||||
* @throws UnexpectedEOFException
|
||||
* @throws UnexpectedTokenException
|
||||
*
|
||||
* @internal since V8.8.0
|
||||
*/
|
||||
public static function parse(ParserState $oParserState, $bIsColorComponent = false)
|
||||
public static function parse(ParserState $parserState, bool $isColorComponent = false): Size
|
||||
{
|
||||
$sSize = '';
|
||||
if ($oParserState->comes('-')) {
|
||||
$sSize .= $oParserState->consume('-');
|
||||
$size = '';
|
||||
if ($parserState->comes('-')) {
|
||||
$size .= $parserState->consume('-');
|
||||
}
|
||||
while (is_numeric($oParserState->peek()) || $oParserState->comes('.') || $oParserState->comes('e', true)) {
|
||||
if ($oParserState->comes('.')) {
|
||||
$sSize .= $oParserState->consume('.');
|
||||
} elseif ($oParserState->comes('e', true)) {
|
||||
$sLookahead = $oParserState->peek(1, 1);
|
||||
if (is_numeric($sLookahead) || $sLookahead === '+' || $sLookahead === '-') {
|
||||
$sSize .= $oParserState->consume(2);
|
||||
while (\is_numeric($parserState->peek()) || $parserState->comes('.') || $parserState->comes('e', true)) {
|
||||
if ($parserState->comes('.')) {
|
||||
$size .= $parserState->consume('.');
|
||||
} elseif ($parserState->comes('e', true)) {
|
||||
$lookahead = $parserState->peek(1, 1);
|
||||
if (\is_numeric($lookahead) || $lookahead === '+' || $lookahead === '-') {
|
||||
$size .= $parserState->consume(2);
|
||||
} else {
|
||||
break; // Reached the unit part of the number like "em" or "ex"
|
||||
}
|
||||
} else {
|
||||
$sSize .= $oParserState->consume(1);
|
||||
$size .= $parserState->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);
|
||||
$unit = null;
|
||||
$sizeUnits = self::getSizeUnits();
|
||||
foreach ($sizeUnits as $length => &$values) {
|
||||
$key = \strtolower($parserState->peek($length));
|
||||
if (\array_key_exists($key, $values)) {
|
||||
if (($unit = $values[$key]) !== null) {
|
||||
$parserState->consume($length);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return new Size((float)$sSize, $sUnit, $bIsColorComponent, $oParserState->currentLine());
|
||||
return new Size((float) $size, $unit, $isColorComponent, $parserState->currentLine());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array<string, string>>
|
||||
* @return array<int<1, max>, array<lowercase-string, non-empty-string>>
|
||||
*/
|
||||
private static function getSizeUnits()
|
||||
private static function getSizeUnits(): array
|
||||
{
|
||||
if (!is_array(self::$SIZE_UNITS)) {
|
||||
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] = [];
|
||||
$sizeUnits = \array_merge(self::ABSOLUTE_SIZE_UNITS, self::RELATIVE_SIZE_UNITS, self::NON_SIZE_UNITS);
|
||||
foreach ($sizeUnits as $sizeUnit) {
|
||||
$tokenLength = \strlen($sizeUnit);
|
||||
if (!isset(self::$SIZE_UNITS[$tokenLength])) {
|
||||
self::$SIZE_UNITS[$tokenLength] = [];
|
||||
}
|
||||
self::$SIZE_UNITS[$iSize][strtolower($val)] = $val;
|
||||
self::$SIZE_UNITS[$tokenLength][\strtolower($sizeUnit)] = $sizeUnit;
|
||||
}
|
||||
|
||||
krsort(self::$SIZE_UNITS, SORT_NUMERIC);
|
||||
\krsort(self::$SIZE_UNITS, SORT_NUMERIC);
|
||||
}
|
||||
|
||||
return self::$SIZE_UNITS;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $sUnit
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setUnit($sUnit)
|
||||
public function setUnit(string $unit): void
|
||||
{
|
||||
$this->sUnit = $sUnit;
|
||||
$this->unit = $unit;
|
||||
}
|
||||
|
||||
public function getUnit(): ?string
|
||||
{
|
||||
return $this->unit;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
* @param float|int|string $size
|
||||
*/
|
||||
public function getUnit()
|
||||
public function setSize($size): void
|
||||
{
|
||||
return $this->sUnit;
|
||||
$this->size = (float) $size;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param float|int|string $fSize
|
||||
*/
|
||||
public function setSize($fSize)
|
||||
public function getSize(): float
|
||||
{
|
||||
$this->fSize = (float)$fSize;
|
||||
return $this->size;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return float
|
||||
*/
|
||||
public function getSize()
|
||||
public function isColorComponent(): bool
|
||||
{
|
||||
return $this->fSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isColorComponent()
|
||||
{
|
||||
return $this->bIsColorComponent;
|
||||
return $this->isColorComponent;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* Returns `false` if the unit is an angle, a duration, a frequency, or the number is a component in a `Color`
|
||||
* object.
|
||||
*/
|
||||
public function isSize()
|
||||
public function isSize(): bool
|
||||
{
|
||||
if (in_array($this->sUnit, self::NON_SIZE_UNITS, true)) {
|
||||
if (\in_array($this->unit, self::NON_SIZE_UNITS, true)) {
|
||||
return false;
|
||||
}
|
||||
return !$this->isColorComponent();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isRelative()
|
||||
public function isRelative(): bool
|
||||
{
|
||||
if (in_array($this->sUnit, self::RELATIVE_SIZE_UNITS, true)) {
|
||||
if (\in_array($this->unit, self::RELATIVE_SIZE_UNITS, true)) {
|
||||
return true;
|
||||
}
|
||||
if ($this->sUnit === null && $this->fSize != 0) {
|
||||
if ($this->unit === null && $this->size !== 0.0) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*
|
||||
* @deprecated in V8.8.0, will be removed in V9.0.0. Use `render` instead.
|
||||
* @return non-empty-string
|
||||
*/
|
||||
public function __toString()
|
||||
public function render(OutputFormat $outputFormat): string
|
||||
{
|
||||
return $this->render(new OutputFormat());
|
||||
$locale = \localeconv();
|
||||
$decimalPoint = \preg_quote($locale['decimal_point'], '/');
|
||||
$size = preg_match('/[\\d\\.]+e[+-]?\\d+/i', (string) $this->size) === 1
|
||||
? preg_replace("/$decimalPoint?0+$/", '', \sprintf('%f', $this->size)) : (string) $this->size;
|
||||
|
||||
return preg_replace(["/$decimalPoint/", '/^(-?)0\\./'], ['.', '$1.'], $size) . ($this->unit ?? '');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param OutputFormat|null $oOutputFormat
|
||||
* @return array<string, bool|int|float|string|array<mixed>|null>
|
||||
*
|
||||
* @return string
|
||||
* @internal
|
||||
*/
|
||||
public function render($oOutputFormat)
|
||||
public function getArrayRepresentation(): array
|
||||
{
|
||||
$l = localeconv();
|
||||
$sPoint = preg_quote($l['decimal_point'], '/');
|
||||
$sSize = preg_match("/[\d\.]+e[+-]?\d+/i", (string)$this->fSize)
|
||||
? preg_replace("/$sPoint?0+$/", "", sprintf("%f", $this->fSize)) : (string)$this->fSize;
|
||||
return preg_replace(["/$sPoint/", "/^(-?)0\./"], ['.', '$1.'], $sSize)
|
||||
. ($this->sUnit === null ? '' : $this->sUnit);
|
||||
return [
|
||||
'class' => $this->getShortClassName(),
|
||||
// 'number' is the official W3C terminology (not 'size')
|
||||
'number' => $this->size,
|
||||
'unit' => $this->unit,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
+48
-48
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sabberworm\CSS\Value;
|
||||
|
||||
use Sabberworm\CSS\OutputFormat;
|
||||
@@ -7,95 +9,93 @@ use Sabberworm\CSS\Parsing\ParserState;
|
||||
use Sabberworm\CSS\Parsing\SourceException;
|
||||
use Sabberworm\CSS\Parsing\UnexpectedEOFException;
|
||||
use Sabberworm\CSS\Parsing\UnexpectedTokenException;
|
||||
use Sabberworm\CSS\ShortClassNameProvider;
|
||||
|
||||
/**
|
||||
* This class represents URLs in CSS. `URL`s always output in `URL("")` notation.
|
||||
*/
|
||||
class URL extends PrimitiveValue
|
||||
{
|
||||
use ShortClassNameProvider;
|
||||
|
||||
/**
|
||||
* @var CSSString
|
||||
*/
|
||||
private $oURL;
|
||||
private $url;
|
||||
|
||||
/**
|
||||
* @param int $iLineNo
|
||||
* @param int<1, max>|null $lineNumber
|
||||
*/
|
||||
public function __construct(CSSString $oURL, $iLineNo = 0)
|
||||
public function __construct(CSSString $url, ?int $lineNumber = null)
|
||||
{
|
||||
parent::__construct($iLineNo);
|
||||
$this->oURL = $oURL;
|
||||
parent::__construct($lineNumber);
|
||||
$this->url = $url;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return URL
|
||||
*
|
||||
* @throws SourceException
|
||||
* @throws UnexpectedEOFException
|
||||
* @throws UnexpectedTokenException
|
||||
*
|
||||
* @internal since V8.8.0
|
||||
*/
|
||||
public static function parse(ParserState $oParserState)
|
||||
public static function parse(ParserState $parserState): URL
|
||||
{
|
||||
$oAnchor = $oParserState->anchor();
|
||||
$sIdentifier = '';
|
||||
$anchor = $parserState->anchor();
|
||||
$identifier = '';
|
||||
for ($i = 0; $i < 3; $i++) {
|
||||
$sChar = $oParserState->parseCharacter(true);
|
||||
if ($sChar === null) {
|
||||
$character = $parserState->parseCharacter(true);
|
||||
if ($character === null) {
|
||||
break;
|
||||
}
|
||||
$sIdentifier .= $sChar;
|
||||
$identifier .= $character;
|
||||
}
|
||||
$bUseUrl = $oParserState->streql($sIdentifier, 'url');
|
||||
if ($bUseUrl) {
|
||||
$oParserState->consumeWhiteSpace();
|
||||
$oParserState->consume('(');
|
||||
$useUrl = $parserState->streql($identifier, 'url');
|
||||
if ($useUrl) {
|
||||
$parserState->consumeWhiteSpace();
|
||||
$parserState->consume('(');
|
||||
} else {
|
||||
$oAnchor->backtrack();
|
||||
$anchor->backtrack();
|
||||
}
|
||||
$oParserState->consumeWhiteSpace();
|
||||
$oResult = new URL(CSSString::parse($oParserState), $oParserState->currentLine());
|
||||
if ($bUseUrl) {
|
||||
$oParserState->consumeWhiteSpace();
|
||||
$oParserState->consume(')');
|
||||
$parserState->consumeWhiteSpace();
|
||||
$result = new URL(CSSString::parse($parserState), $parserState->currentLine());
|
||||
if ($useUrl) {
|
||||
$parserState->consumeWhiteSpace();
|
||||
$parserState->consume(')');
|
||||
}
|
||||
return $oResult;
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function setURL(CSSString $oURL)
|
||||
public function setURL(CSSString $url): void
|
||||
{
|
||||
$this->oURL = $oURL;
|
||||
$this->url = $url;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return CSSString
|
||||
*/
|
||||
public function getURL()
|
||||
public function getURL(): CSSString
|
||||
{
|
||||
return $this->oURL;
|
||||
return $this->url;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
* @return non-empty-string
|
||||
*/
|
||||
public function render(OutputFormat $outputFormat): string
|
||||
{
|
||||
return "url({$this->url->render($outputFormat)})";
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, bool|int|float|string|array<mixed>|null>
|
||||
*
|
||||
* @deprecated in V8.8.0, will be removed in V9.0.0. Use `render` instead.
|
||||
* @internal
|
||||
*/
|
||||
public function __toString()
|
||||
public function getArrayRepresentation(): array
|
||||
{
|
||||
return $this->render(new OutputFormat());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param OutputFormat|null $oOutputFormat
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function render($oOutputFormat)
|
||||
{
|
||||
return "url({$this->oURL->render($oOutputFormat)})";
|
||||
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/syndata.html#uri
|
||||
'uri' => $this->url->getArrayRepresentation(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
+115
-105
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sabberworm\CSS\Value;
|
||||
|
||||
use Sabberworm\CSS\CSSElement;
|
||||
@@ -9,6 +11,9 @@ use Sabberworm\CSS\Parsing\UnexpectedEOFException;
|
||||
use Sabberworm\CSS\Parsing\UnexpectedTokenException;
|
||||
use Sabberworm\CSS\Position\Position;
|
||||
use Sabberworm\CSS\Position\Positionable;
|
||||
use Sabberworm\CSS\ShortClassNameProvider;
|
||||
|
||||
use function Safe\preg_match;
|
||||
|
||||
/**
|
||||
* Abstract base class for specific classes of CSS values: `Size`, `Color`, `CSSString` and `URL`, and another
|
||||
@@ -17,96 +22,94 @@ use Sabberworm\CSS\Position\Positionable;
|
||||
abstract class Value implements CSSElement, Positionable
|
||||
{
|
||||
use Position;
|
||||
use ShortClassNameProvider;
|
||||
|
||||
/**
|
||||
* @param int $iLineNo
|
||||
* @param int<1, max>|null $lineNumber
|
||||
*/
|
||||
public function __construct($iLineNo = 0)
|
||||
public function __construct(?int $lineNumber = null)
|
||||
{
|
||||
$this->setPosition($iLineNo);
|
||||
$this->setPosition($lineNumber);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<array-key, string> $aListDelimiters
|
||||
* @param array<non-empty-string> $listDelimiters
|
||||
*
|
||||
* @return RuleValueList|CSSFunction|CSSString|LineName|Size|URL|string
|
||||
* @return Value|string
|
||||
*
|
||||
* @throws UnexpectedTokenException
|
||||
* @throws UnexpectedEOFException
|
||||
*
|
||||
* @internal since V8.8.0
|
||||
*/
|
||||
public static function parseValue(ParserState $oParserState, array $aListDelimiters = [])
|
||||
public static function parseValue(ParserState $parserState, array $listDelimiters = [])
|
||||
{
|
||||
/** @var array<int, RuleValueList|CSSFunction|CSSString|LineName|Size|URL|string> $aStack */
|
||||
$aStack = [];
|
||||
$oParserState->consumeWhiteSpace();
|
||||
/** @var list<Value|string> $stack */
|
||||
$stack = [];
|
||||
$parserState->consumeWhiteSpace();
|
||||
//Build a list of delimiters and parsed values
|
||||
while (
|
||||
!($oParserState->comes('}') || $oParserState->comes(';') || $oParserState->comes('!')
|
||||
|| $oParserState->comes(')')
|
||||
|| $oParserState->comes('\\')
|
||||
|| $oParserState->isEnd())
|
||||
!($parserState->comes('}') || $parserState->comes(';') || $parserState->comes('!')
|
||||
|| $parserState->comes(')')
|
||||
|| $parserState->isEnd())
|
||||
) {
|
||||
if (count($aStack) > 0) {
|
||||
$bFoundDelimiter = false;
|
||||
foreach ($aListDelimiters as $sDelimiter) {
|
||||
if ($oParserState->comes($sDelimiter)) {
|
||||
array_push($aStack, $oParserState->consume($sDelimiter));
|
||||
$oParserState->consumeWhiteSpace();
|
||||
$bFoundDelimiter = true;
|
||||
if (\count($stack) > 0) {
|
||||
$foundDelimiter = false;
|
||||
foreach ($listDelimiters as $delimiter) {
|
||||
if ($parserState->comes($delimiter)) {
|
||||
\array_push($stack, $parserState->consume($delimiter));
|
||||
$parserState->consumeWhiteSpace();
|
||||
$foundDelimiter = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!$bFoundDelimiter) {
|
||||
if (!$foundDelimiter) {
|
||||
//Whitespace was the list delimiter
|
||||
array_push($aStack, ' ');
|
||||
\array_push($stack, ' ');
|
||||
}
|
||||
}
|
||||
array_push($aStack, self::parsePrimitiveValue($oParserState));
|
||||
$oParserState->consumeWhiteSpace();
|
||||
\array_push($stack, self::parsePrimitiveValue($parserState));
|
||||
$parserState->consumeWhiteSpace();
|
||||
}
|
||||
// Convert the list to list objects
|
||||
foreach ($aListDelimiters as $sDelimiter) {
|
||||
$iStackLength = count($aStack);
|
||||
if ($iStackLength === 1) {
|
||||
return $aStack[0];
|
||||
foreach ($listDelimiters as $delimiter) {
|
||||
$stackSize = \count($stack);
|
||||
if ($stackSize === 1) {
|
||||
return $stack[0];
|
||||
}
|
||||
$aNewStack = [];
|
||||
for ($iStartPosition = 0; $iStartPosition < $iStackLength; ++$iStartPosition) {
|
||||
if ($iStartPosition === ($iStackLength - 1) || $sDelimiter !== $aStack[$iStartPosition + 1]) {
|
||||
$aNewStack[] = $aStack[$iStartPosition];
|
||||
$newStack = [];
|
||||
for ($offset = 0; $offset < $stackSize; ++$offset) {
|
||||
if ($offset === ($stackSize - 1) || $delimiter !== $stack[$offset + 1]) {
|
||||
$newStack[] = $stack[$offset];
|
||||
continue;
|
||||
}
|
||||
$iLength = 2; //Number of elements to be joined
|
||||
for ($i = $iStartPosition + 3; $i < $iStackLength; $i += 2, ++$iLength) {
|
||||
if ($sDelimiter !== $aStack[$i]) {
|
||||
$length = 2; //Number of elements to be joined
|
||||
for ($i = $offset + 3; $i < $stackSize; $i += 2, ++$length) {
|
||||
if ($delimiter !== $stack[$i]) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
$oList = new RuleValueList($sDelimiter, $oParserState->currentLine());
|
||||
for ($i = $iStartPosition; $i - $iStartPosition < $iLength * 2; $i += 2) {
|
||||
$oList->addListComponent($aStack[$i]);
|
||||
$list = new RuleValueList($delimiter, $parserState->currentLine());
|
||||
for ($i = $offset; $i - $offset < $length * 2; $i += 2) {
|
||||
$list->addListComponent($stack[$i]);
|
||||
}
|
||||
$aNewStack[] = $oList;
|
||||
$iStartPosition += $iLength * 2 - 2;
|
||||
$newStack[] = $list;
|
||||
$offset += $length * 2 - 2;
|
||||
}
|
||||
$aStack = $aNewStack;
|
||||
$stack = $newStack;
|
||||
}
|
||||
if (!isset($aStack[0])) {
|
||||
if (!isset($stack[0])) {
|
||||
throw new UnexpectedTokenException(
|
||||
" {$oParserState->peek()} ",
|
||||
$oParserState->peek(1, -1) . $oParserState->peek(2),
|
||||
" {$parserState->peek()} ",
|
||||
$parserState->peek(1, -1) . $parserState->peek(2),
|
||||
'literal',
|
||||
$oParserState->currentLine()
|
||||
$parserState->currentLine()
|
||||
);
|
||||
}
|
||||
return $aStack[0];
|
||||
return $stack[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $bIgnoreCase
|
||||
*
|
||||
* @return CSSFunction|string
|
||||
*
|
||||
* @throws UnexpectedEOFException
|
||||
@@ -114,27 +117,23 @@ abstract class Value implements CSSElement, Positionable
|
||||
*
|
||||
* @internal since V8.8.0
|
||||
*/
|
||||
public static function parseIdentifierOrFunction(ParserState $oParserState, $bIgnoreCase = false)
|
||||
public static function parseIdentifierOrFunction(ParserState $parserState, bool $ignoreCase = false)
|
||||
{
|
||||
$oAnchor = $oParserState->anchor();
|
||||
$mResult = $oParserState->parseIdentifier($bIgnoreCase);
|
||||
$anchor = $parserState->anchor();
|
||||
$result = $parserState->parseIdentifier($ignoreCase);
|
||||
|
||||
if ($oParserState->comes('(')) {
|
||||
$oAnchor->backtrack();
|
||||
if ($oParserState->streql('url', $mResult)) {
|
||||
$mResult = URL::parse($oParserState);
|
||||
} elseif (
|
||||
$oParserState->streql('calc', $mResult)
|
||||
|| $oParserState->streql('-webkit-calc', $mResult)
|
||||
|| $oParserState->streql('-moz-calc', $mResult)
|
||||
) {
|
||||
$mResult = CalcFunction::parse($oParserState);
|
||||
if ($parserState->comes('(')) {
|
||||
$anchor->backtrack();
|
||||
if ($parserState->streql('url', $result)) {
|
||||
$result = URL::parse($parserState);
|
||||
} elseif ($parserState->streql('calc', $result)) {
|
||||
$result = CalcFunction::parse($parserState);
|
||||
} else {
|
||||
$mResult = CSSFunction::parse($oParserState, $bIgnoreCase);
|
||||
$result = CSSFunction::parse($parserState, $ignoreCase);
|
||||
}
|
||||
}
|
||||
|
||||
return $mResult;
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -146,73 +145,84 @@ abstract class Value implements CSSElement, Positionable
|
||||
*
|
||||
* @internal since V8.8.0
|
||||
*/
|
||||
public static function parsePrimitiveValue(ParserState $oParserState)
|
||||
public static function parsePrimitiveValue(ParserState $parserState)
|
||||
{
|
||||
$oValue = null;
|
||||
$oParserState->consumeWhiteSpace();
|
||||
$parserState->consumeWhiteSpace();
|
||||
if (
|
||||
is_numeric($oParserState->peek())
|
||||
|| ($oParserState->comes('-.')
|
||||
&& is_numeric($oParserState->peek(1, 2)))
|
||||
|| (($oParserState->comes('-') || $oParserState->comes('.')) && is_numeric($oParserState->peek(1, 1)))
|
||||
\is_numeric($parserState->peek())
|
||||
|| ($parserState->comes('-.')
|
||||
&& \is_numeric($parserState->peek(1, 2)))
|
||||
|| (($parserState->comes('-') || $parserState->comes('.')) && \is_numeric($parserState->peek(1, 1)))
|
||||
) {
|
||||
$oValue = Size::parse($oParserState);
|
||||
} elseif ($oParserState->comes('#') || $oParserState->comes('rgb', true) || $oParserState->comes('hsl', true)) {
|
||||
$oValue = Color::parse($oParserState);
|
||||
} elseif ($oParserState->comes("'") || $oParserState->comes('"')) {
|
||||
$oValue = CSSString::parse($oParserState);
|
||||
} elseif ($oParserState->comes("progid:") && $oParserState->getSettings()->bLenientParsing) {
|
||||
$oValue = self::parseMicrosoftFilter($oParserState);
|
||||
} elseif ($oParserState->comes("[")) {
|
||||
$oValue = LineName::parse($oParserState);
|
||||
} elseif ($oParserState->comes("U+")) {
|
||||
$oValue = self::parseUnicodeRangeValue($oParserState);
|
||||
$value = Size::parse($parserState);
|
||||
} elseif ($parserState->comes('#') || $parserState->comes('rgb', true) || $parserState->comes('hsl', true)) {
|
||||
$value = Color::parse($parserState);
|
||||
} elseif ($parserState->comes("'") || $parserState->comes('"')) {
|
||||
$value = CSSString::parse($parserState);
|
||||
} elseif ($parserState->comes('progid:') && $parserState->getSettings()->usesLenientParsing()) {
|
||||
$value = self::parseMicrosoftFilter($parserState);
|
||||
} elseif ($parserState->comes('[')) {
|
||||
$value = LineName::parse($parserState);
|
||||
} elseif ($parserState->comes('U+')) {
|
||||
$value = self::parseUnicodeRangeValue($parserState);
|
||||
} else {
|
||||
$sNextChar = $oParserState->peek(1);
|
||||
$nextCharacter = $parserState->peek(1);
|
||||
try {
|
||||
$oValue = self::parseIdentifierOrFunction($oParserState);
|
||||
$value = self::parseIdentifierOrFunction($parserState);
|
||||
} catch (UnexpectedTokenException $e) {
|
||||
if (\in_array($sNextChar, ['+', '-', '*', '/'], true)) {
|
||||
$oValue = $oParserState->consume(1);
|
||||
if (\in_array($nextCharacter, ['+', '-', '*', '/'], true)) {
|
||||
$value = $parserState->consume(1);
|
||||
} else {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
$oParserState->consumeWhiteSpace();
|
||||
return $oValue;
|
||||
$parserState->consumeWhiteSpace();
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return CSSFunction
|
||||
* @return array<string, bool|int|float|string|array<mixed>|null>
|
||||
*
|
||||
* @throws UnexpectedEOFException
|
||||
* @throws UnexpectedTokenException
|
||||
* @internal
|
||||
*/
|
||||
private static function parseMicrosoftFilter(ParserState $oParserState)
|
||||
public function getArrayRepresentation(): array
|
||||
{
|
||||
$sFunction = $oParserState->consumeUntil('(', false, true);
|
||||
$aArguments = Value::parseValue($oParserState, [',', '=']);
|
||||
return new CSSFunction($sFunction, $aArguments, ',', $oParserState->currentLine());
|
||||
return [
|
||||
'class' => $this->getShortClassName(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*
|
||||
* @throws UnexpectedEOFException
|
||||
* @throws UnexpectedTokenException
|
||||
*/
|
||||
private static function parseUnicodeRangeValue(ParserState $oParserState)
|
||||
private static function parseMicrosoftFilter(ParserState $parserState): CSSFunction
|
||||
{
|
||||
$iCodepointMaxLength = 6; // Code points outside BMP can use up to six digits
|
||||
$sRange = "";
|
||||
$oParserState->consume("U+");
|
||||
$function = $parserState->consumeUntil('(', false, true);
|
||||
$arguments = Value::parseValue($parserState, [',', '=']);
|
||||
return new CSSFunction($function, $arguments, ',', $parserState->currentLine());
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws UnexpectedEOFException
|
||||
* @throws UnexpectedTokenException
|
||||
*/
|
||||
private static function parseUnicodeRangeValue(ParserState $parserState): string
|
||||
{
|
||||
$codepointMaxLength = 6; // Code points outside BMP can use up to six digits
|
||||
$range = '';
|
||||
$parserState->consume('U+');
|
||||
do {
|
||||
if ($oParserState->comes('-')) {
|
||||
$iCodepointMaxLength = 13; // Max length is 2 six digit code points + the dash(-) between them
|
||||
if ($parserState->comes('-')) {
|
||||
$codepointMaxLength = 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}";
|
||||
$range .= $parserState->consume(1);
|
||||
} while (
|
||||
(\strlen($range) < $codepointMaxLength) && (preg_match('/[A-Fa-f0-9\\?-]/', $parserState->peek()) === 1)
|
||||
);
|
||||
|
||||
return "U+{$range}";
|
||||
}
|
||||
}
|
||||
|
||||
+64
-54
@@ -1,8 +1,11 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sabberworm\CSS\Value;
|
||||
|
||||
use Sabberworm\CSS\OutputFormat;
|
||||
use Sabberworm\CSS\ShortClassNameProvider;
|
||||
|
||||
/**
|
||||
* A `ValueList` represents a lists of `Value`s, separated by some separation character
|
||||
@@ -12,103 +15,110 @@ use Sabberworm\CSS\OutputFormat;
|
||||
*/
|
||||
abstract class ValueList extends Value
|
||||
{
|
||||
use ShortClassNameProvider;
|
||||
|
||||
/**
|
||||
* @var array<int, RuleValueList|CSSFunction|CSSString|LineName|Size|URL|string>
|
||||
* @var array<Value|string>
|
||||
*
|
||||
* @internal since 8.8.0
|
||||
*/
|
||||
protected $aComponents;
|
||||
protected $components;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
* @var non-empty-string
|
||||
*
|
||||
* @internal since 8.8.0
|
||||
*/
|
||||
protected $sSeparator;
|
||||
protected $separator;
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @param array<Value|string>|Value|string $components
|
||||
* @param non-empty-string $separator
|
||||
* @param int<1, max>|null $lineNumber
|
||||
*/
|
||||
public function __construct($aComponents = [], $sSeparator = ',', $iLineNo = 0)
|
||||
public function __construct($components = [], $separator = ',', ?int $lineNumber = null)
|
||||
{
|
||||
parent::__construct($iLineNo);
|
||||
if (!is_array($aComponents)) {
|
||||
$aComponents = [$aComponents];
|
||||
parent::__construct($lineNumber);
|
||||
if (!\is_array($components)) {
|
||||
$components = [$components];
|
||||
}
|
||||
$this->aComponents = $aComponents;
|
||||
$this->sSeparator = $sSeparator;
|
||||
$this->components = $components;
|
||||
$this->separator = $separator;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param RuleValueList|CSSFunction|CSSString|LineName|Size|URL|string $mComponent
|
||||
*
|
||||
* @return void
|
||||
* @param Value|string $component
|
||||
*/
|
||||
public function addListComponent($mComponent)
|
||||
public function addListComponent($component): void
|
||||
{
|
||||
$this->aComponents[] = $mComponent;
|
||||
$this->components[] = $component;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, RuleValueList|CSSFunction|CSSString|LineName|Size|URL|string>
|
||||
* @return array<Value|string>
|
||||
*/
|
||||
public function getListComponents()
|
||||
public function getListComponents(): array
|
||||
{
|
||||
return $this->aComponents;
|
||||
return $this->components;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, RuleValueList|CSSFunction|CSSString|LineName|Size|URL|string> $aComponents
|
||||
*
|
||||
* @return void
|
||||
* @param array<Value|string> $components
|
||||
*/
|
||||
public function setListComponents(array $aComponents)
|
||||
public function setListComponents(array $components): void
|
||||
{
|
||||
$this->aComponents = $aComponents;
|
||||
$this->components = $components;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
* @return non-empty-string
|
||||
*/
|
||||
public function getListSeparator()
|
||||
public function getListSeparator(): string
|
||||
{
|
||||
return $this->sSeparator;
|
||||
return $this->separator;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $sSeparator
|
||||
*
|
||||
* @return void
|
||||
* @param non-empty-string $separator
|
||||
*/
|
||||
public function setListSeparator($sSeparator)
|
||||
public function setListSeparator(string $separator): void
|
||||
{
|
||||
$this->sSeparator = $sSeparator;
|
||||
$this->separator = $separator;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*
|
||||
* @deprecated in V8.8.0, will be removed in V9.0.0. Use `render` instead.
|
||||
*/
|
||||
public function __toString()
|
||||
public function render(OutputFormat $outputFormat): string
|
||||
{
|
||||
return $this->render(new OutputFormat());
|
||||
}
|
||||
$formatter = $outputFormat->getFormatter();
|
||||
|
||||
/**
|
||||
* @param OutputFormat|null $oOutputFormat
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function render($oOutputFormat)
|
||||
{
|
||||
return $oOutputFormat->implode(
|
||||
$oOutputFormat->spaceBeforeListArgumentSeparator($this->sSeparator) . $this->sSeparator
|
||||
. $oOutputFormat->spaceAfterListArgumentSeparator($this->sSeparator),
|
||||
$this->aComponents
|
||||
return $formatter->implode(
|
||||
$formatter->spaceBeforeListArgumentSeparator($this->separator) . $this->separator
|
||||
. $formatter->spaceAfterListArgumentSeparator($this->separator),
|
||||
$this->components
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, bool|int|float|string|array<mixed>|null>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public function getArrayRepresentation(): array
|
||||
{
|
||||
return [
|
||||
'class' => $this->getShortClassName(),
|
||||
'components' => \array_map(
|
||||
/**
|
||||
* @parm Value|string $component
|
||||
*/
|
||||
function ($component): array {
|
||||
if (\is_string($component)) {
|
||||
return ['class' => 'string', 'value' => $component];
|
||||
}
|
||||
return $component->getArrayRepresentation();
|
||||
},
|
||||
$this->components
|
||||
),
|
||||
'separator' => $this->separator,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user