From c8ee3c905143f40ef69fe78028580dcbc54798c1 Mon Sep 17 00:00:00 2001 From: David Woloszyn Date: Fri, 14 Oct 2022 13:11:25 +1100 Subject: [PATCH 1/2] MDL-75475 lib: Upgraded PHP CSS Parser to 8.4.0 --- .../CSSList/AtRuleBlockList.php | 103 +- lib/php-css-parser/CSSList/CSSBlockList.php | 220 +-- lib/php-css-parser/CSSList/CSSList.php | 756 +++++---- lib/php-css-parser/CSSList/Document.php | 246 +-- lib/php-css-parser/CSSList/KeyFrame.php | 124 +- lib/php-css-parser/Comment/Comment.php | 94 +- lib/php-css-parser/Comment/Commentable.php | 34 +- lib/php-css-parser/OutputFormat.php | 620 ++++---- lib/php-css-parser/OutputFormatter.php | 231 +++ lib/php-css-parser/Parser.php | 73 +- .../Parsing/OutputException.php | 20 +- lib/php-css-parser/Parsing/ParserState.php | 756 +++++---- .../Parsing/SourceException.php | 40 +- .../Parsing/UnexpectedEOFException.php | 12 + .../Parsing/UnexpectedTokenException.php | 68 +- lib/php-css-parser/Property/AtRule.php | 38 +- lib/php-css-parser/Property/CSSNamespace.php | 197 ++- lib/php-css-parser/Property/Charset.php | 157 +- lib/php-css-parser/Property/Import.php | 174 ++- .../Property/KeyframeSelector.php | 23 + lib/php-css-parser/Property/Selector.php | 180 ++- lib/php-css-parser/Renderable.php | 22 +- lib/php-css-parser/Rule/Rule.php | 549 ++++--- lib/php-css-parser/RuleSet/AtRuleSet.php | 89 +- .../RuleSet/DeclarationBlock.php | 1387 ++++++++++------- lib/php-css-parser/RuleSet/RuleSet.php | 472 +++--- lib/php-css-parser/Settings.php | 117 +- lib/php-css-parser/Value/CSSFunction.php | 89 +- lib/php-css-parser/Value/CSSString.php | 145 +- lib/php-css-parser/Value/CalcFunction.php | 131 +- .../Value/CalcRuleValueList.php | 24 +- lib/php-css-parser/Value/Color.php | 229 ++- lib/php-css-parser/Value/LineName.php | 84 +- lib/php-css-parser/Value/PrimitiveValue.php | 12 +- lib/php-css-parser/Value/RuleValueList.php | 16 +- lib/php-css-parser/Value/Size.php | 279 ++-- lib/php-css-parser/Value/URL.php | 107 +- lib/php-css-parser/Value/Value.php | 293 ++-- lib/php-css-parser/Value/ValueList.php | 117 +- lib/thirdpartylibs.xml | 2 +- 40 files changed, 5244 insertions(+), 3086 deletions(-) create mode 100644 lib/php-css-parser/OutputFormatter.php create mode 100644 lib/php-css-parser/Parsing/UnexpectedEOFException.php create mode 100644 lib/php-css-parser/Property/KeyframeSelector.php diff --git a/lib/php-css-parser/CSSList/AtRuleBlockList.php b/lib/php-css-parser/CSSList/AtRuleBlockList.php index 24e79f02f49..218adb9a196 100644 --- a/lib/php-css-parser/CSSList/AtRuleBlockList.php +++ b/lib/php-css-parser/CSSList/AtRuleBlockList.php @@ -2,49 +2,82 @@ namespace Sabberworm\CSS\CSSList; +use Sabberworm\CSS\OutputFormat; use Sabberworm\CSS\Property\AtRule; /** - * A BlockList constructed by an unknown @-rule. @media rules are rendered into AtRuleBlockList objects. + * A `BlockList` constructed by an unknown at-rule. `@media` rules are rendered into `AtRuleBlockList` objects. */ -class AtRuleBlockList extends CSSBlockList implements AtRule { +class AtRuleBlockList extends CSSBlockList implements AtRule +{ + /** + * @var string + */ + private $sType; - private $sType; - private $sArgs; + /** + * @var string + */ + private $sArgs; - public function __construct($sType, $sArgs = '', $iLineNo = 0) { - parent::__construct($iLineNo); - $this->sType = $sType; - $this->sArgs = $sArgs; - } + /** + * @param string $sType + * @param string $sArgs + * @param int $iLineNo + */ + public function __construct($sType, $sArgs = '', $iLineNo = 0) + { + parent::__construct($iLineNo); + $this->sType = $sType; + $this->sArgs = $sArgs; + } - public function atRuleName() { - return $this->sType; - } + /** + * @return string + */ + public function atRuleName() + { + return $this->sType; + } - public function atRuleArgs() { - return $this->sArgs; - } + /** + * @return string + */ + public function atRuleArgs() + { + return $this->sArgs; + } - public function __toString() { - return $this->render(new \Sabberworm\CSS\OutputFormat()); - } + /** + * @return string + */ + public function __toString() + { + return $this->render(new OutputFormat()); + } - public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) { - $sArgs = $this->sArgs; - if($sArgs) { - $sArgs = ' ' . $sArgs; - } - $sResult = $oOutputFormat->sBeforeAtRuleBlock; - $sResult .= "@{$this->sType}$sArgs{$oOutputFormat->spaceBeforeOpeningBrace()}{"; - $sResult .= parent::render($oOutputFormat); - $sResult .= '}'; - $sResult .= $oOutputFormat->sAfterAtRuleBlock; - return $sResult; - } + /** + * @return string + */ + public function render(OutputFormat $oOutputFormat) + { + $sArgs = $this->sArgs; + if ($sArgs) { + $sArgs = ' ' . $sArgs; + } + $sResult = $oOutputFormat->sBeforeAtRuleBlock; + $sResult .= "@{$this->sType}$sArgs{$oOutputFormat->spaceBeforeOpeningBrace()}{"; + $sResult .= parent::render($oOutputFormat); + $sResult .= '}'; + $sResult .= $oOutputFormat->sAfterAtRuleBlock; + return $sResult; + } - public function isRootList() { - return false; - } - -} \ No newline at end of file + /** + * @return bool + */ + public function isRootList() + { + return false; + } +} diff --git a/lib/php-css-parser/CSSList/CSSBlockList.php b/lib/php-css-parser/CSSList/CSSBlockList.php index 15742423deb..fce7913eb96 100644 --- a/lib/php-css-parser/CSSList/CSSBlockList.php +++ b/lib/php-css-parser/CSSList/CSSBlockList.php @@ -2,106 +2,142 @@ namespace Sabberworm\CSS\CSSList; -use Sabberworm\CSS\RuleSet\DeclarationBlock; -use Sabberworm\CSS\RuleSet\RuleSet; use Sabberworm\CSS\Property\Selector; use Sabberworm\CSS\Rule\Rule; -use Sabberworm\CSS\Value\ValueList; +use Sabberworm\CSS\RuleSet\DeclarationBlock; +use Sabberworm\CSS\RuleSet\RuleSet; use Sabberworm\CSS\Value\CSSFunction; +use Sabberworm\CSS\Value\Value; +use Sabberworm\CSS\Value\ValueList; /** - * A CSSBlockList is a CSSList whose DeclarationBlocks are guaranteed to contain valid declaration blocks or at-rules. - * Most CSSLists conform to this category but some at-rules (such as @keyframes) do not. + * A `CSSBlockList` is a `CSSList` whose `DeclarationBlock`s are guaranteed to contain valid declaration blocks or + * at-rules. + * + * Most `CSSList`s conform to this category but some at-rules (such as `@keyframes`) do not. */ -abstract class CSSBlockList extends CSSList { - public function __construct($iLineNo = 0) { - parent::__construct($iLineNo); - } +abstract class CSSBlockList extends CSSList +{ + /** + * @param int $iLineNo + */ + public function __construct($iLineNo = 0) + { + parent::__construct($iLineNo); + } - protected function allDeclarationBlocks(&$aResult) { - foreach ($this->aContents as $mContent) { - if ($mContent instanceof DeclarationBlock) { - $aResult[] = $mContent; - } else if ($mContent instanceof CSSBlockList) { - $mContent->allDeclarationBlocks($aResult); - } - } - } + /** + * @param array $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); + } + } + } - protected function allRuleSets(&$aResult) { - foreach ($this->aContents as $mContent) { - if ($mContent instanceof RuleSet) { - $aResult[] = $mContent; - } else if ($mContent instanceof CSSBlockList) { - $mContent->allRuleSets($aResult); - } - } - } + /** + * @param array $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); + } + } + } - protected function allValues($oElement, &$aResult, $sSearchString = null, $bSearchInFunctionArguments = false) { - if ($oElement instanceof CSSBlockList) { - foreach ($oElement->getContents() as $oContent) { - $this->allValues($oContent, $aResult, $sSearchString, $bSearchInFunctionArguments); - } - } else if ($oElement instanceof RuleSet) { - foreach ($oElement->getRules($sSearchString) as $oRule) { - $this->allValues($oRule, $aResult, $sSearchString, $bSearchInFunctionArguments); - } - } else if ($oElement instanceof Rule) { - $this->allValues($oElement->getValue(), $aResult, $sSearchString, $bSearchInFunctionArguments); - } else if ($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; - } - } - - protected function allSelectors(&$aResult, $sSpecificitySearch = null) { - $aDeclarationBlocks = array(); - $this->allDeclarationBlocks($aDeclarationBlocks); - foreach ($aDeclarationBlocks as $oBlock) { - foreach ($oBlock->getSelectors() as $oSelector) { - if ($sSpecificitySearch === null) { - $aResult[] = $oSelector; - } else { - $sComparator = '==='; - $aSpecificitySearch = explode(' ', $sSpecificitySearch); - $iTargetSpecificity = $aSpecificitySearch[0]; - if(count($aSpecificitySearch) > 1) { - $sComparator = $aSpecificitySearch[0]; - $iTargetSpecificity = $aSpecificitySearch[1]; - } - $iTargetSpecificity = (int)$iTargetSpecificity; - $iSelectorSpecificity = $oSelector->getSpecificity(); - $bMatches = false; - switch($sComparator) { - case '<=': - $bMatches = $iSelectorSpecificity <= $iTargetSpecificity; - break; - case '<': - $bMatches = $iSelectorSpecificity < $iTargetSpecificity; - break; - case '>=': - $bMatches = $iSelectorSpecificity >= $iTargetSpecificity; - break; - case '>': - $bMatches = $iSelectorSpecificity > $iTargetSpecificity; - break; - default: - $bMatches = $iSelectorSpecificity === $iTargetSpecificity; - break; - } - if ($bMatches) { - $aResult[] = $oSelector; - } - } - } - } - } + /** + * @param CSSList|Rule|RuleSet|Value $oElement + * @param array $aResult + * @param string|null $sSearchString + * @param bool $bSearchInFunctionArguments + * + * @return void + */ + protected function allValues($oElement, array &$aResult, $sSearchString = null, $bSearchInFunctionArguments = false) + { + if ($oElement instanceof CSSBlockList) { + foreach ($oElement->getContents() as $oContent) { + $this->allValues($oContent, $aResult, $sSearchString, $bSearchInFunctionArguments); + } + } elseif ($oElement instanceof RuleSet) { + foreach ($oElement->getRules($sSearchString) as $oRule) { + $this->allValues($oRule, $aResult, $sSearchString, $bSearchInFunctionArguments); + } + } elseif ($oElement instanceof Rule) { + $this->allValues($oElement->getValue(), $aResult, $sSearchString, $bSearchInFunctionArguments); + } elseif ($oElement instanceof ValueList) { + if ($bSearchInFunctionArguments || !($oElement instanceof CSSFunction)) { + foreach ($oElement->getListComponents() as $mComponent) { + $this->allValues($mComponent, $aResult, $sSearchString, $bSearchInFunctionArguments); + } + } + } else { + // Non-List `Value` or `CSSString` (CSS identifier) + $aResult[] = $oElement; + } + } + /** + * @param array $aResult + * @param string|null $sSpecificitySearch + * + * @return void + */ + protected function allSelectors(array &$aResult, $sSpecificitySearch = null) + { + /** @var array $aDeclarationBlocks */ + $aDeclarationBlocks = []; + $this->allDeclarationBlocks($aDeclarationBlocks); + foreach ($aDeclarationBlocks as $oBlock) { + foreach ($oBlock->getSelectors() as $oSelector) { + if ($sSpecificitySearch === null) { + $aResult[] = $oSelector; + } else { + $sComparator = '==='; + $aSpecificitySearch = explode(' ', $sSpecificitySearch); + $iTargetSpecificity = $aSpecificitySearch[0]; + if (count($aSpecificitySearch) > 1) { + $sComparator = $aSpecificitySearch[0]; + $iTargetSpecificity = $aSpecificitySearch[1]; + } + $iTargetSpecificity = (int)$iTargetSpecificity; + $iSelectorSpecificity = $oSelector->getSpecificity(); + $bMatches = false; + switch ($sComparator) { + case '<=': + $bMatches = $iSelectorSpecificity <= $iTargetSpecificity; + break; + case '<': + $bMatches = $iSelectorSpecificity < $iTargetSpecificity; + break; + case '>=': + $bMatches = $iSelectorSpecificity >= $iTargetSpecificity; + break; + case '>': + $bMatches = $iSelectorSpecificity > $iTargetSpecificity; + break; + default: + $bMatches = $iSelectorSpecificity === $iTargetSpecificity; + break; + } + if ($bMatches) { + $aResult[] = $oSelector; + } + } + } + } + } } diff --git a/lib/php-css-parser/CSSList/CSSList.php b/lib/php-css-parser/CSSList/CSSList.php index 11e1dcc8259..946740a4999 100644 --- a/lib/php-css-parser/CSSList/CSSList.php +++ b/lib/php-css-parser/CSSList/CSSList.php @@ -2,9 +2,12 @@ namespace Sabberworm\CSS\CSSList; +use Sabberworm\CSS\Comment\Comment; use Sabberworm\CSS\Comment\Commentable; +use Sabberworm\CSS\OutputFormat; use Sabberworm\CSS\Parsing\ParserState; use Sabberworm\CSS\Parsing\SourceException; +use Sabberworm\CSS\Parsing\UnexpectedEOFException; use Sabberworm\CSS\Parsing\UnexpectedTokenException; use Sabberworm\CSS\Property\AtRule; use Sabberworm\CSS\Property\Charset; @@ -15,347 +18,462 @@ use Sabberworm\CSS\Renderable; use Sabberworm\CSS\RuleSet\AtRuleSet; use Sabberworm\CSS\RuleSet\DeclarationBlock; use Sabberworm\CSS\RuleSet\RuleSet; +use Sabberworm\CSS\Settings; use Sabberworm\CSS\Value\CSSString; use Sabberworm\CSS\Value\URL; use Sabberworm\CSS\Value\Value; /** - * A CSSList is the most generic container available. Its contents include RuleSet as well as other CSSList objects. - * Also, it may contain Import and Charset objects stemming from @-rules. + * A `CSSList` is the most generic container available. Its contents include `RuleSet` as well as other `CSSList` + * objects. + * + * Also, it may contain `Import` and `Charset` objects stemming from at-rules. */ -abstract class CSSList implements Renderable, Commentable { - - protected $aComments; - protected $aContents; - protected $iLineNo; - - public function __construct($iLineNo = 0) { - $this->aComments = array(); - $this->aContents = array(); - $this->iLineNo = $iLineNo; - } - - public static function parseList(ParserState $oParserState, CSSList $oList) { - $bIsRoot = $oList instanceof Document; - if(is_string($oParserState)) { - $oParserState = new ParserState($oParserState); - } - $bLenientParsing = $oParserState->getSettings()->bLenientParsing; - while(!$oParserState->isEnd()) { - $comments = $oParserState->consumeWhiteSpace(); - $oListItem = null; - if($bLenientParsing) { - try { - $oListItem = self::parseListItem($oParserState, $oList); - } catch (UnexpectedTokenException $e) { - $oListItem = false; - } - } else { - $oListItem = self::parseListItem($oParserState, $oList); - } - if($oListItem === null) { - // List parsing finished - return; - } - if($oListItem) { - $oListItem->setComments($comments); - $oList->append($oListItem); - } - } - if(!$bIsRoot && !$bLenientParsing) { - throw new SourceException("Unexpected end of document", $oParserState->currentLine()); - } - } - - private static function parseListItem(ParserState $oParserState, CSSList $oList) { - $bIsRoot = $oList instanceof Document; - if ($oParserState->comes('@')) { - $oAtRule = self::parseAtRule($oParserState); - if($oAtRule instanceof Charset) { - if(!$bIsRoot) { - throw new UnexpectedTokenException('@charset may only occur in root document', '', 'custom', $oParserState->currentLine()); - } - if(count($oList->getContents()) > 0) { - throw new UnexpectedTokenException('@charset must be the first parseable token in a document', '', 'custom', $oParserState->currentLine()); - } - $oParserState->setCharset($oAtRule->getCharset()->getString()); - } - return $oAtRule; - } else if ($oParserState->comes('}')) { - $oParserState->consume('}'); - if ($bIsRoot) { - if ($oParserState->getSettings()->bLenientParsing) { - while ($oParserState->comes('}')) $oParserState->consume('}'); - return DeclarationBlock::parse($oParserState); - } else { - throw new SourceException("Unopened {", $oParserState->currentLine()); - } - } else { - return null; - } - } else { - return DeclarationBlock::parse($oParserState); - } - } - - private static function parseAtRule(ParserState $oParserState) { - $oParserState->consume('@'); - $sIdentifier = $oParserState->parseIdentifier(); - $iIdentifierLineNum = $oParserState->currentLine(); - $oParserState->consumeWhiteSpace(); - if ($sIdentifier === 'import') { - $oLocation = URL::parse($oParserState); - $oParserState->consumeWhiteSpace(); - $sMediaQuery = null; - if (!$oParserState->comes(';')) { - $sMediaQuery = $oParserState->consumeUntil(';'); - } - $oParserState->consume(';'); - return new Import($oLocation, $sMediaQuery, $iIdentifierLineNum); - } else if ($sIdentifier === 'charset') { - $sCharset = CSSString::parse($oParserState); - $oParserState->consumeWhiteSpace(); - $oParserState->consume(';'); - return new Charset($sCharset, $iIdentifierLineNum); - } else if (self::identifierIs($sIdentifier, 'keyframes')) { - $oResult = new KeyFrame($iIdentifierLineNum); - $oResult->setVendorKeyFrame($sIdentifier); - $oResult->setAnimationName(trim($oParserState->consumeUntil('{', false, true))); - CSSList::parseList($oParserState, $oResult); - return $oResult; - } else if ($sIdentifier === 'namespace') { - $sPrefix = null; - $mUrl = Value::parsePrimitiveValue($oParserState); - if (!$oParserState->comes(';')) { - $sPrefix = $mUrl; - $mUrl = Value::parsePrimitiveValue($oParserState); - } - $oParserState->consume(';'); - if ($sPrefix !== null && !is_string($sPrefix)) { - throw new UnexpectedTokenException('Wrong namespace prefix', $sPrefix, 'custom', $iIdentifierLineNum); - } - if (!($mUrl instanceof CSSString || $mUrl instanceof URL)) { - throw new UnexpectedTokenException('Wrong namespace url of invalid type', $mUrl, 'custom', $iIdentifierLineNum); - } - return new CSSNamespace($mUrl, $sPrefix, $iIdentifierLineNum); - } else { - //Unknown other at rule (font-face or such) - $sArgs = trim($oParserState->consumeUntil('{', false, true)); - if (substr_count($sArgs, "(") != substr_count($sArgs, ")")) { - if($oParserState->getSettings()->bLenientParsing) { - return NULL; - } else { - throw new SourceException("Unmatched brace count in media query", $oParserState->currentLine()); - } - } - $bUseRuleSet = true; - foreach(explode('/', AtRule::BLOCK_RULES) as $sBlockRuleName) { - if(self::identifierIs($sIdentifier, $sBlockRuleName)) { - $bUseRuleSet = false; - break; - } - } - if($bUseRuleSet) { - $oAtRule = new AtRuleSet($sIdentifier, $sArgs, $iIdentifierLineNum); - RuleSet::parseRuleSet($oParserState, $oAtRule); - } else { - $oAtRule = new AtRuleBlockList($sIdentifier, $sArgs, $iIdentifierLineNum); - CSSList::parseList($oParserState, $oAtRule); - } - return $oAtRule; - } - } - - /** - * Tests an identifier for a given value. Since identifiers are all keywords, they can be vendor-prefixed. We need to check for these versions too. - */ - private static function identifierIs($sIdentifier, $sMatch) { - return (strcasecmp($sIdentifier, $sMatch) === 0) - ?: preg_match("/^(-\\w+-)?$sMatch$/i", $sIdentifier) === 1; - } - - - /** - * @return int - */ - public function getLineNo() { - return $this->iLineNo; - } - - /** - * Prepend item to list of contents. - * - * @param object $oItem Item. - */ - public function prepend($oItem) { - array_unshift($this->aContents, $oItem); - } - - /** - * Append item to list of contents. - * - * @param object $oItem Item. - */ - public function append($oItem) { - $this->aContents[] = $oItem; - } - - /** - * Splice the list of contents. - * - * @param int $iOffset Offset. - * @param int $iLength Length. Optional. - * @param RuleSet[] $mReplacement Replacement. Optional. - */ - public function splice($iOffset, $iLength = null, $mReplacement = null) { - array_splice($this->aContents, $iOffset, $iLength, $mReplacement); - } +abstract class CSSList implements Renderable, Commentable +{ + /** + * @var array + */ + protected $aComments; /** - * Insert an item before its sibling. - * - * @param mixed $oItem The item. - * @param mixed $oSibling The sibling. + * @var array */ - public function insert($oItem, $oSibling) { - $iIndex = array_search($oSibling, $this->aContents); - if ($iIndex === false) { - return $this->append($oItem); - } - array_splice($this->aContents, $iIndex, 0, array($oItem)); + protected $aContents; + + /** + * @var int + */ + protected $iLineNo; + + /** + * @param int $iLineNo + */ + public function __construct($iLineNo = 0) + { + $this->aComments = []; + $this->aContents = []; + $this->iLineNo = $iLineNo; } - /** - * Removes an item from the CSS list. - * @param RuleSet|Import|Charset|CSSList $oItemToRemove May be a RuleSet (most likely a DeclarationBlock), a Import, a Charset or another CSSList (most likely a MediaQuery) - * @return bool Whether the item was removed. - */ - public function remove($oItemToRemove) { - $iKey = array_search($oItemToRemove, $this->aContents, true); - if ($iKey !== false) { - unset($this->aContents[$iKey]); - return true; - } - return false; - } + /** + * @return void + * + * @throws UnexpectedTokenException + * @throws SourceException + */ + public static function parseList(ParserState $oParserState, CSSList $oList) + { + $bIsRoot = $oList instanceof Document; + if (is_string($oParserState)) { + $oParserState = new ParserState($oParserState, Settings::create()); + } + $bLenientParsing = $oParserState->getSettings()->bLenientParsing; + while (!$oParserState->isEnd()) { + $comments = $oParserState->consumeWhiteSpace(); + $oListItem = null; + if ($bLenientParsing) { + try { + $oListItem = self::parseListItem($oParserState, $oList); + } catch (UnexpectedTokenException $e) { + $oListItem = false; + } + } else { + $oListItem = self::parseListItem($oParserState, $oList); + } + if ($oListItem === null) { + // List parsing finished + return; + } + if ($oListItem) { + $oListItem->setComments($comments); + $oList->append($oListItem); + } + $oParserState->consumeWhiteSpace(); + } + if (!$bIsRoot && !$bLenientParsing) { + throw new SourceException("Unexpected end of document", $oParserState->currentLine()); + } + } - /** - * Replaces 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) - */ - public function replace($oOldItem, $oNewItem) { - $iKey = array_search($oOldItem, $this->aContents, true); - if ($iKey !== false) { - array_splice($this->aContents, $iKey, 1, $oNewItem); - return true; - } - return false; - } + /** + * @return AtRuleBlockList|KeyFrame|Charset|CSSNamespace|Import|AtRuleSet|DeclarationBlock|null|false + * + * @throws SourceException + * @throws UnexpectedEOFException + * @throws UnexpectedTokenException + */ + private static function parseListItem(ParserState $oParserState, CSSList $oList) + { + $bIsRoot = $oList instanceof Document; + if ($oParserState->comes('@')) { + $oAtRule = self::parseAtRule($oParserState); + if ($oAtRule instanceof Charset) { + if (!$bIsRoot) { + throw new UnexpectedTokenException( + '@charset may only occur in root document', + '', + 'custom', + $oParserState->currentLine() + ); + } + if (count($oList->getContents()) > 0) { + throw new UnexpectedTokenException( + '@charset must be the first parseable token in a document', + '', + 'custom', + $oParserState->currentLine() + ); + } + $oParserState->setCharset($oAtRule->getCharset()->getString()); + } + return $oAtRule; + } elseif ($oParserState->comes('}')) { + if (!$oParserState->getSettings()->bLenientParsing) { + throw new UnexpectedTokenException('CSS selector', '}', 'identifier', $oParserState->currentLine()); + } else { + if ($bIsRoot) { + if ($oParserState->getSettings()->bLenientParsing) { + return DeclarationBlock::parse($oParserState); + } else { + throw new SourceException("Unopened {", $oParserState->currentLine()); + } + } else { + return null; + } + } + } else { + return DeclarationBlock::parse($oParserState, $oList); + } + } - /** - * Set the contents. - * @param array $aContents Objects to set as content. - */ - public function setContents(array $aContents) { - $this->aContents = array(); - foreach ($aContents as $content) { - $this->append($content); - } - } + /** + * @param ParserState $oParserState + * + * @return AtRuleBlockList|KeyFrame|Charset|CSSNamespace|Import|AtRuleSet|null + * + * @throws SourceException + * @throws UnexpectedTokenException + * @throws UnexpectedEOFException + */ + private static function parseAtRule(ParserState $oParserState) + { + $oParserState->consume('@'); + $sIdentifier = $oParserState->parseIdentifier(); + $iIdentifierLineNum = $oParserState->currentLine(); + $oParserState->consumeWhiteSpace(); + if ($sIdentifier === 'import') { + $oLocation = URL::parse($oParserState); + $oParserState->consumeWhiteSpace(); + $sMediaQuery = null; + if (!$oParserState->comes(';')) { + $sMediaQuery = trim($oParserState->consumeUntil([';', ParserState::EOF])); + } + $oParserState->consumeUntil([';', ParserState::EOF], true, true); + return new Import($oLocation, $sMediaQuery ?: null, $iIdentifierLineNum); + } elseif ($sIdentifier === 'charset') { + $sCharset = CSSString::parse($oParserState); + $oParserState->consumeWhiteSpace(); + $oParserState->consumeUntil([';', ParserState::EOF], true, true); + return new Charset($sCharset, $iIdentifierLineNum); + } elseif (self::identifierIs($sIdentifier, 'keyframes')) { + $oResult = new KeyFrame($iIdentifierLineNum); + $oResult->setVendorKeyFrame($sIdentifier); + $oResult->setAnimationName(trim($oParserState->consumeUntil('{', false, true))); + CSSList::parseList($oParserState, $oResult); + if ($oParserState->comes('}')) { + $oParserState->consume('}'); + } + return $oResult; + } elseif ($sIdentifier === 'namespace') { + $sPrefix = null; + $mUrl = Value::parsePrimitiveValue($oParserState); + if (!$oParserState->comes(';')) { + $sPrefix = $mUrl; + $mUrl = Value::parsePrimitiveValue($oParserState); + } + $oParserState->consumeUntil([';', ParserState::EOF], true, true); + if ($sPrefix !== null && !is_string($sPrefix)) { + throw new UnexpectedTokenException('Wrong namespace prefix', $sPrefix, 'custom', $iIdentifierLineNum); + } + if (!($mUrl instanceof CSSString || $mUrl instanceof URL)) { + throw new UnexpectedTokenException( + 'Wrong namespace url of invalid type', + $mUrl, + 'custom', + $iIdentifierLineNum + ); + } + return new CSSNamespace($mUrl, $sPrefix, $iIdentifierLineNum); + } else { + // Unknown other at rule (font-face or such) + $sArgs = trim($oParserState->consumeUntil('{', false, true)); + if (substr_count($sArgs, "(") != substr_count($sArgs, ")")) { + if ($oParserState->getSettings()->bLenientParsing) { + return null; + } else { + throw new SourceException("Unmatched brace count in media query", $oParserState->currentLine()); + } + } + $bUseRuleSet = true; + foreach (explode('/', AtRule::BLOCK_RULES) as $sBlockRuleName) { + if (self::identifierIs($sIdentifier, $sBlockRuleName)) { + $bUseRuleSet = false; + break; + } + } + if ($bUseRuleSet) { + $oAtRule = new AtRuleSet($sIdentifier, $sArgs, $iIdentifierLineNum); + RuleSet::parseRuleSet($oParserState, $oAtRule); + } else { + $oAtRule = new AtRuleBlockList($sIdentifier, $sArgs, $iIdentifierLineNum); + CSSList::parseList($oParserState, $oAtRule); + if ($oParserState->comes('}')) { + $oParserState->consume('}'); + } + } + return $oAtRule; + } + } - /** - * Removes a declaration block from the CSS list if it matches all given selectors. - * @param array|string $mSelector The selectors to match. - * @param boolean $bRemoveAll Whether to stop at the first declaration block found or remove all blocks - */ - public function removeDeclarationBlockBySelector($mSelector, $bRemoveAll = false) { - if ($mSelector instanceof DeclarationBlock) { - $mSelector = $mSelector->getSelectors(); - } - if (!is_array($mSelector)) { - $mSelector = explode(',', $mSelector); - } - foreach ($mSelector as $iKey => &$mSel) { - if (!($mSel instanceof Selector)) { - $mSel = new Selector($mSel); - } - } - foreach ($this->aContents as $iKey => $mItem) { - if (!($mItem instanceof DeclarationBlock)) { - continue; - } - if ($mItem->getSelectors() == $mSelector) { - unset($this->aContents[$iKey]); - if (!$bRemoveAll) { - return; - } - } - } - } + /** + * Tests an identifier for a given value. Since identifiers are all keywords, they can be vendor-prefixed. + * We need to check for these versions too. + * + * @param string $sIdentifier + * @param string $sMatch + * + * @return bool + */ + private static function identifierIs($sIdentifier, $sMatch) + { + return (strcasecmp($sIdentifier, $sMatch) === 0) + ?: preg_match("/^(-\\w+-)?$sMatch$/i", $sIdentifier) === 1; + } - public function __toString() { - return $this->render(new \Sabberworm\CSS\OutputFormat()); - } + /** + * @return int + */ + public function getLineNo() + { + return $this->iLineNo; + } - public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) { - $sResult = ''; - $bIsFirst = true; - $oNextLevel = $oOutputFormat; - if(!$this->isRootList()) { - $oNextLevel = $oOutputFormat->nextLevel(); - } - foreach ($this->aContents as $oContent) { - $sRendered = $oOutputFormat->safely(function() use ($oNextLevel, $oContent) { - return $oContent->render($oNextLevel); - }); - if($sRendered === null) { - continue; - } - if($bIsFirst) { - $bIsFirst = false; - $sResult .= $oNextLevel->spaceBeforeBlocks(); - } else { - $sResult .= $oNextLevel->spaceBetweenBlocks(); - } - $sResult .= $sRendered; - } + /** + * Prepends an item to the list of contents. + * + * @param RuleSet|CSSList|Import|Charset $oItem + * + * @return void + */ + public function prepend($oItem) + { + array_unshift($this->aContents, $oItem); + } - if(!$bIsFirst) { - // Had some output - $sResult .= $oOutputFormat->spaceAfterBlocks(); - } + /** + * Appends an item to tje list of contents. + * + * @param RuleSet|CSSList|Import|Charset $oItem + * + * @return void + */ + public function append($oItem) + { + $this->aContents[] = $oItem; + } - return $sResult; - } + /** + * Splices the list of contents. + * + * @param int $iOffset + * @param int $iLength + * @param array $mReplacement + * + * @return void + */ + public function splice($iOffset, $iLength = null, $mReplacement = null) + { + array_splice($this->aContents, $iOffset, $iLength, $mReplacement); + } - /** - * Return true if the list can not be further outdented. Only important when rendering. - */ - public abstract function isRootList(); + /** + * Removes an item from the CSS list. + * + * @param RuleSet|Import|Charset|CSSList $oItemToRemove + * May be a RuleSet (most likely a DeclarationBlock), a Import, + * a Charset or another CSSList (most likely a MediaQuery) + * + * @return bool whether the item was removed + */ + public function remove($oItemToRemove) + { + $iKey = array_search($oItemToRemove, $this->aContents, true); + if ($iKey !== false) { + unset($this->aContents[$iKey]); + return true; + } + return false; + } - public function getContents() { - return $this->aContents; - } + /** + * Replaces an item from the CSS list. + * + * @param RuleSet|Import|Charset|CSSList $oOldItem + * May be a `RuleSet` (most likely a `DeclarationBlock`), an `Import`, a `Charset` + * or another `CSSList` (most likely a `MediaQuery`) + * + * @return bool + */ + public function replace($oOldItem, $mNewItem) + { + $iKey = array_search($oOldItem, $this->aContents, true); + if ($iKey !== false) { + if (is_array($mNewItem)) { + array_splice($this->aContents, $iKey, 1, $mNewItem); + } else { + array_splice($this->aContents, $iKey, 1, [$mNewItem]); + } + return true; + } + return false; + } - /** - * @param array $aComments Array of comments. - */ - public function addComments(array $aComments) { - $this->aComments = array_merge($this->aComments, $aComments); - } + /** + * @param array $aContents + */ + public function setContents(array $aContents) + { + $this->aContents = []; + foreach ($aContents as $content) { + $this->append($content); + } + } - /** - * @return array - */ - public function getComments() { - return $this->aComments; - } + /** + * Removes a declaration block from the CSS list if it matches all given selectors. + * + * @param DeclarationBlock|array|string $mSelector the selectors to match + * @param bool $bRemoveAll whether to stop at the first declaration block found or remove all blocks + * + * @return void + */ + public function removeDeclarationBlockBySelector($mSelector, $bRemoveAll = false) + { + if ($mSelector instanceof DeclarationBlock) { + $mSelector = $mSelector->getSelectors(); + } + if (!is_array($mSelector)) { + $mSelector = explode(',', $mSelector); + } + foreach ($mSelector as $iKey => &$mSel) { + if (!($mSel instanceof Selector)) { + if (!Selector::isValid($mSel)) { + throw new UnexpectedTokenException( + "Selector did not match '" . Selector::SELECTOR_VALIDATION_RX . "'.", + $mSel, + "custom" + ); + } + $mSel = new Selector($mSel); + } + } + foreach ($this->aContents as $iKey => $mItem) { + if (!($mItem instanceof DeclarationBlock)) { + continue; + } + if ($mItem->getSelectors() == $mSelector) { + unset($this->aContents[$iKey]); + if (!$bRemoveAll) { + return; + } + } + } + } - /** - * @param array $aComments Array containing Comment objects. - */ - public function setComments(array $aComments) { - $this->aComments = $aComments; - } + /** + * @return string + */ + public function __toString() + { + return $this->render(new OutputFormat()); + } + /** + * @return string + */ + public function render(OutputFormat $oOutputFormat) + { + $sResult = ''; + $bIsFirst = true; + $oNextLevel = $oOutputFormat; + if (!$this->isRootList()) { + $oNextLevel = $oOutputFormat->nextLevel(); + } + foreach ($this->aContents as $oContent) { + $sRendered = $oOutputFormat->safely(function () use ($oNextLevel, $oContent) { + return $oContent->render($oNextLevel); + }); + if ($sRendered === null) { + continue; + } + if ($bIsFirst) { + $bIsFirst = false; + $sResult .= $oNextLevel->spaceBeforeBlocks(); + } else { + $sResult .= $oNextLevel->spaceBetweenBlocks(); + } + $sResult .= $sRendered; + } + + if (!$bIsFirst) { + // Had some output + $sResult .= $oOutputFormat->spaceAfterBlocks(); + } + + return $sResult; + } + + /** + * Return true if the list can not be further outdented. Only important when rendering. + * + * @return bool + */ + abstract public function isRootList(); + + /** + * @return array + */ + public function getContents() + { + return $this->aContents; + } + + /** + * @param array $aComments + * + * @return void + */ + public function addComments(array $aComments) + { + $this->aComments = array_merge($this->aComments, $aComments); + } + + /** + * @return array + */ + public function getComments() + { + return $this->aComments; + } + + /** + * @param array $aComments + * + * @return void + */ + public function setComments(array $aComments) + { + $this->aComments = $aComments; + } } diff --git a/lib/php-css-parser/CSSList/Document.php b/lib/php-css-parser/CSSList/Document.php index 1658aee8dbb..91ab2c6b2e7 100644 --- a/lib/php-css-parser/CSSList/Document.php +++ b/lib/php-css-parser/CSSList/Document.php @@ -2,109 +2,171 @@ namespace Sabberworm\CSS\CSSList; +use Sabberworm\CSS\OutputFormat; use Sabberworm\CSS\Parsing\ParserState; +use Sabberworm\CSS\Parsing\SourceException; +use Sabberworm\CSS\Property\Selector; +use Sabberworm\CSS\RuleSet\DeclarationBlock; +use Sabberworm\CSS\RuleSet\RuleSet; +use Sabberworm\CSS\Value\Value; /** - * The root CSSList of a parsed file. Contains all top-level css contents, mostly declaration blocks, but also any @-rules encountered. + * The root `CSSList` of a parsed file. Contains all top-level CSS contents, mostly declaration blocks, + * but also any at-rules encountered. */ -class Document extends CSSBlockList { - /** - * Document constructor. - * @param int $iLineNo - */ - public function __construct($iLineNo = 0) { - parent::__construct($iLineNo); - } +class Document extends CSSBlockList +{ + /** + * @param int $iLineNo + */ + public function __construct($iLineNo = 0) + { + parent::__construct($iLineNo); + } - public static function parse(ParserState $oParserState) { - $oDocument = new Document($oParserState->currentLine()); - CSSList::parseList($oParserState, $oDocument); - return $oDocument; - } + /** + * @return Document + * + * @throws SourceException + */ + public static function parse(ParserState $oParserState) + { + $oDocument = new Document($oParserState->currentLine()); + CSSList::parseList($oParserState, $oDocument); + return $oDocument; + } - /** - * Gets all DeclarationBlock objects recursively. - */ - public function getAllDeclarationBlocks() { - $aResult = array(); - $this->allDeclarationBlocks($aResult); - return $aResult; - } + /** + * Gets all `DeclarationBlock` objects recursively. + * + * @return array + */ + public function getAllDeclarationBlocks() + { + /** @var array $aResult */ + $aResult = []; + $this->allDeclarationBlocks($aResult); + return $aResult; + } - /** - * @deprecated use getAllDeclarationBlocks() - */ - public function getAllSelectors() { - return $this->getAllDeclarationBlocks(); - } + /** + * Gets all `DeclarationBlock` objects recursively. + * + * @return array + * + * @deprecated will be removed in version 9.0; use `getAllDeclarationBlocks()` instead + */ + public function getAllSelectors() + { + return $this->getAllDeclarationBlocks(); + } - /** - * Returns all RuleSet objects found recursively in the tree. - */ - public function getAllRuleSets() { - $aResult = array(); - $this->allRuleSets($aResult); - return $aResult; - } + /** + * Returns all `RuleSet` objects found recursively in the tree. + * + * @return array + */ + public function getAllRuleSets() + { + /** @var array $aResult */ + $aResult = []; + $this->allRuleSets($aResult); + return $aResult; + } - /** - * Returns all Value objects found recursively in the tree. - * @param (object|string) $mElement the CSSList or RuleSet to start the search from (defaults to the whole document). If a string is given, it is used as rule name filter (@see{RuleSet->getRules()}). - * @param (bool) $bSearchInFunctionArguments whether to also return Value objects used as Function arguments. - */ - public function getAllValues($mElement = null, $bSearchInFunctionArguments = false) { - $sSearchString = null; - if ($mElement === null) { - $mElement = $this; - } else if (is_string($mElement)) { - $sSearchString = $mElement; - $mElement = $this; - } - $aResult = array(); - $this->allValues($mElement, $aResult, $sSearchString, $bSearchInFunctionArguments); - return $aResult; - } + /** + * Returns all `Value` objects found recursively in the tree. + * + * @param CSSList|RuleSet|string $mElement + * the `CSSList` or `RuleSet` to start the search from (defaults to the whole document). + * If a string is given, it is used as rule name filter. + * @param bool $bSearchInFunctionArguments whether to also return Value objects used as Function arguments. + * + * @return array + * + * @see RuleSet->getRules() + */ + public function getAllValues($mElement = null, $bSearchInFunctionArguments = false) + { + $sSearchString = null; + if ($mElement === null) { + $mElement = $this; + } elseif (is_string($mElement)) { + $sSearchString = $mElement; + $mElement = $this; + } + /** @var array $aResult */ + $aResult = []; + $this->allValues($mElement, $aResult, $sSearchString, $bSearchInFunctionArguments); + return $aResult; + } - /** - * Returns all Selector objects found recursively in the tree. - * Note that this does not yield the full DeclarationBlock that the selector belongs to (and, currently, there is no way to get to that). - * @param $sSpecificitySearch An optional filter by specificity. May contain a comparison operator and a number or just a number (defaults to "=="). - * @example getSelectorsBySpecificity('>= 100') - */ - public function getSelectorsBySpecificity($sSpecificitySearch = null) { - $aResult = array(); - $this->allSelectors($aResult, $sSpecificitySearch); - return $aResult; - } + /** + * Returns all `Selector` objects found recursively in the tree. + * + * Note that this does not yield the full `DeclarationBlock` that the selector belongs to + * (and, currently, there is no way to get to that). + * + * @param string|null $sSpecificitySearch + * An optional filter by specificity. + * May contain a comparison operator and a number or just a number (defaults to "=="). + * + * @return array + * @example `getSelectorsBySpecificity('>= 100')` + * + */ + public function getSelectorsBySpecificity($sSpecificitySearch = null) + { + /** @var array $aResult */ + $aResult = []; + $this->allSelectors($aResult, $sSpecificitySearch); + return $aResult; + } - /** - * Expands all shorthand properties to their long value - */ - public function expandShorthands() { - foreach ($this->getAllDeclarationBlocks() as $oDeclaration) { - $oDeclaration->expandShorthands(); - } - } + /** + * Expands all shorthand properties to their long value. + * + * @return void + */ + public function expandShorthands() + { + foreach ($this->getAllDeclarationBlocks() as $oDeclaration) { + $oDeclaration->expandShorthands(); + } + } - /** - * Create shorthands properties whenever possible - */ - public function createShorthands() { - foreach ($this->getAllDeclarationBlocks() as $oDeclaration) { - $oDeclaration->createShorthands(); - } - } + /** + * Create shorthands properties whenever possible. + * + * @return void + */ + public function createShorthands() + { + foreach ($this->getAllDeclarationBlocks() as $oDeclaration) { + $oDeclaration->createShorthands(); + } + } - // Override render() to make format argument optional - public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat = null) { - if($oOutputFormat === null) { - $oOutputFormat = new \Sabberworm\CSS\OutputFormat(); - } - return parent::render($oOutputFormat); - } + /** + * Overrides `render()` to make format argument optional. + * + * @param OutputFormat|null $oOutputFormat + * + * @return string + */ + public function render(OutputFormat $oOutputFormat = null) + { + if ($oOutputFormat === null) { + $oOutputFormat = new OutputFormat(); + } + return parent::render($oOutputFormat); + } - public function isRootList() { - return true; - } - -} \ No newline at end of file + /** + * @return bool + */ + public function isRootList() + { + return true; + } +} diff --git a/lib/php-css-parser/CSSList/KeyFrame.php b/lib/php-css-parser/CSSList/KeyFrame.php index 0334b1b3f18..d9420e9c0d4 100644 --- a/lib/php-css-parser/CSSList/KeyFrame.php +++ b/lib/php-css-parser/CSSList/KeyFrame.php @@ -2,55 +2,103 @@ namespace Sabberworm\CSS\CSSList; +use Sabberworm\CSS\OutputFormat; use Sabberworm\CSS\Property\AtRule; -class KeyFrame extends CSSList implements AtRule { +class KeyFrame extends CSSList implements AtRule +{ + /** + * @var string|null + */ + private $vendorKeyFrame; - private $vendorKeyFrame; - private $animationName; + /** + * @var string|null + */ + private $animationName; - public function __construct($iLineNo = 0) { - parent::__construct($iLineNo); - $this->vendorKeyFrame = null; - $this->animationName = null; - } + /** + * @param int $iLineNo + */ + public function __construct($iLineNo = 0) + { + parent::__construct($iLineNo); + $this->vendorKeyFrame = null; + $this->animationName = null; + } - public function setVendorKeyFrame($vendorKeyFrame) { - $this->vendorKeyFrame = $vendorKeyFrame; - } + /** + * @param string $vendorKeyFrame + */ + public function setVendorKeyFrame($vendorKeyFrame) + { + $this->vendorKeyFrame = $vendorKeyFrame; + } - public function getVendorKeyFrame() { - return $this->vendorKeyFrame; - } + /** + * @return string|null + */ + public function getVendorKeyFrame() + { + return $this->vendorKeyFrame; + } - public function setAnimationName($animationName) { - $this->animationName = $animationName; - } + /** + * @param string $animationName + */ + public function setAnimationName($animationName) + { + $this->animationName = $animationName; + } - public function getAnimationName() { - return $this->animationName; - } + /** + * @return string|null + */ + public function getAnimationName() + { + return $this->animationName; + } - public function __toString() { - return $this->render(new \Sabberworm\CSS\OutputFormat()); - } + /** + * @return string + */ + public function __toString() + { + return $this->render(new OutputFormat()); + } - public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) { - $sResult = "@{$this->vendorKeyFrame} {$this->animationName}{$oOutputFormat->spaceBeforeOpeningBrace()}{"; - $sResult .= parent::render($oOutputFormat); - $sResult .= '}'; - return $sResult; - } + /** + * @return string + */ + public function render(OutputFormat $oOutputFormat) + { + $sResult = "@{$this->vendorKeyFrame} {$this->animationName}{$oOutputFormat->spaceBeforeOpeningBrace()}{"; + $sResult .= parent::render($oOutputFormat); + $sResult .= '}'; + return $sResult; + } - public function isRootList() { - return false; - } + /** + * @return bool + */ + public function isRootList() + { + return false; + } - public function atRuleName() { - return $this->vendorKeyFrame; - } + /** + * @return string|null + */ + public function atRuleName() + { + return $this->vendorKeyFrame; + } - public function atRuleArgs() { - return $this->animationName; - } + /** + * @return string|null + */ + public function atRuleArgs() + { + return $this->animationName; + } } diff --git a/lib/php-css-parser/Comment/Comment.php b/lib/php-css-parser/Comment/Comment.php index 70521b16a3d..6128d749878 100644 --- a/lib/php-css-parser/Comment/Comment.php +++ b/lib/php-css-parser/Comment/Comment.php @@ -2,50 +2,70 @@ namespace Sabberworm\CSS\Comment; +use Sabberworm\CSS\OutputFormat; use Sabberworm\CSS\Renderable; -class Comment implements Renderable { - protected $iLineNo; - protected $sComment; +class Comment implements Renderable +{ + /** + * @var int + */ + protected $iLineNo; - public function __construct($sComment = '', $iLineNo = 0) { - $this->sComment = $sComment; - $this->iLineNo = $iLineNo; - } + /** + * @var string + */ + protected $sComment; - /** - * @return string - */ - public function getComment() { - return $this->sComment; - } + /** + * @param string $sComment + * @param int $iLineNo + */ + public function __construct($sComment = '', $iLineNo = 0) + { + $this->sComment = $sComment; + $this->iLineNo = $iLineNo; + } - /** - * @return int - */ - public function getLineNo() { - return $this->iLineNo; - } + /** + * @return string + */ + public function getComment() + { + return $this->sComment; + } - /** - * @return string - */ - public function setComment($sComment) { - $this->sComment = $sComment; - } + /** + * @return int + */ + public function getLineNo() + { + return $this->iLineNo; + } - /** - * @return string - */ - public function __toString() { - return $this->render(new \Sabberworm\CSS\OutputFormat()); - } + /** + * @param string $sComment + * + * @return void + */ + public function setComment($sComment) + { + $this->sComment = $sComment; + } - /** - * @return string - */ - public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) { - return '/*' . $this->sComment . '*/'; - } + /** + * @return string + */ + public function __toString() + { + return $this->render(new OutputFormat()); + } + /** + * @return string + */ + public function render(OutputFormat $oOutputFormat) + { + return '/*' . $this->sComment . '*/'; + } } diff --git a/lib/php-css-parser/Comment/Commentable.php b/lib/php-css-parser/Comment/Commentable.php index 3100f17a8fa..5e450bfb392 100644 --- a/lib/php-css-parser/Comment/Commentable.php +++ b/lib/php-css-parser/Comment/Commentable.php @@ -2,22 +2,24 @@ namespace Sabberworm\CSS\Comment; -interface Commentable { - - /** - * @param array $aComments Array of comments. - */ - public function addComments(array $aComments); - - /** - * @return array - */ - public function getComments(); - - /** - * @param array $aComments Array containing Comment objects. - */ - public function setComments(array $aComments); +interface Commentable +{ + /** + * @param array $aComments + * + * @return void + */ + public function addComments(array $aComments); + /** + * @return array + */ + public function getComments(); + /** + * @param array $aComments + * + * @return void + */ + public function setComments(array $aComments); } diff --git a/lib/php-css-parser/OutputFormat.php b/lib/php-css-parser/OutputFormat.php index f7ebb5a571a..595d306431c 100644 --- a/lib/php-css-parser/OutputFormat.php +++ b/lib/php-css-parser/OutputFormat.php @@ -2,321 +2,333 @@ namespace Sabberworm\CSS; -use Sabberworm\CSS\Parsing\OutputException; - /** * Class OutputFormat * - * @method OutputFormat setSemicolonAfterLastRule( bool $bSemicolonAfterLastRule ) Set whether semicolons are added after last rule. + * @method OutputFormat setSemicolonAfterLastRule(bool $bSemicolonAfterLastRule) Set whether semicolons are added after + * last rule. */ -class OutputFormat { - /** - * Value format - */ - // " means double-quote, ' means single-quote - public $sStringQuotingType = '"'; - // Output RGB colors in hash notation if possible - public $bRGBHashNotation = true; - - /** - * Declaration format - */ - // Semicolon after the last rule of a declaration block can be omitted. To do that, set this false. - public $bSemicolonAfterLastRule = true; - - /** - * Spacing - * Note that these strings are not sanity-checked: the value should only consist of whitespace - * Any newline character will be indented according to the current level. - * The triples (After, Before, Between) can be set using a wildcard (e.g. `$oFormat->set('Space*Rules', "\n");`) - */ - public $sSpaceAfterRuleName = ' '; +class OutputFormat +{ + /** + * Value format: `"` means double-quote, `'` means single-quote + * + * @var string + */ + public $sStringQuotingType = '"'; - public $sSpaceBeforeRules = ''; - public $sSpaceAfterRules = ''; - public $sSpaceBetweenRules = ''; + /** + * Output RGB colors in hash notation if possible + * + * @var string + */ + public $bRGBHashNotation = true; - public $sSpaceBeforeBlocks = ''; - public $sSpaceAfterBlocks = ''; - public $sSpaceBetweenBlocks = "\n"; + /** + * Declaration format + * + * Semicolon after the last rule of a declaration block can be omitted. To do that, set this false. + * + * @var bool + */ + public $bSemicolonAfterLastRule = true; - // Content injected in and around @-rule blocks. - public $sBeforeAtRuleBlock = ''; - public $sAfterAtRuleBlock = ''; + /** + * Spacing + * Note that these strings are not sanity-checked: the value should only consist of whitespace + * Any newline character will be indented according to the current level. + * The triples (After, Before, Between) can be set using a wildcard (e.g. `$oFormat->set('Space*Rules', "\n");`) + */ + public $sSpaceAfterRuleName = ' '; - // This is what’s printed before and after the comma if a declaration block contains multiple selectors. - public $sSpaceBeforeSelectorSeparator = ''; - public $sSpaceAfterSelectorSeparator = ' '; - // This is what’s printed after the comma of value lists - public $sSpaceBeforeListArgumentSeparator = ''; - public $sSpaceAfterListArgumentSeparator = ''; - - public $sSpaceBeforeOpeningBrace = ' '; + /** + * @var string + */ + public $sSpaceBeforeRules = ''; - // Content injected in and around declaration blocks. - public $sBeforeDeclarationBlock = ''; - public $sAfterDeclarationBlockSelectors = ''; - public $sAfterDeclarationBlock = ''; + /** + * @var string + */ + public $sSpaceAfterRules = ''; - /** - * Indentation - */ - // Indentation character(s) per level. Only applicable if newlines are used in any of the spacing settings. - public $sIndentation = "\t"; - - /** - * Output exceptions. - */ - public $bIgnoreExceptions = false; - - - private $oFormatter = null; - private $oNextLevelFormat = null; - private $iIndentationLevel = 0; - - public function __construct() { - } - - public function get($sName) { - $aVarPrefixes = array('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; - } - - public function set($aNames, $mValue) { - $aVarPrefixes = array('a', 's', 'm', 'b', 'f', 'o', 'c', 'i'); - if(is_string($aNames) && strpos($aNames, '*') !== false) { - $aNames = array(str_replace('*', 'Before', $aNames), str_replace('*', 'Between', $aNames), str_replace('*', 'After', $aNames)); - } else if(!is_array($aNames)) { - $aNames = array($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; - } - - public function __call($sMethodName, $aArguments) { - if(strpos($sMethodName, 'set') === 0) { - return $this->set(substr($sMethodName, 3), $aArguments[0]); - } else if(strpos($sMethodName, 'get') === 0) { - return $this->get(substr($sMethodName, 3)); - } else if(method_exists('\\Sabberworm\\CSS\\OutputFormatter', $sMethodName)) { - return call_user_func_array(array($this->getFormatter(), $sMethodName), $aArguments); - } else { - throw new \Exception('Unknown OutputFormat method called: '.$sMethodName); - } - } - - public function indentWithTabs($iNumber = 1) { - return $this->setIndentation(str_repeat("\t", $iNumber)); - } - - public function indentWithSpaces($iNumber = 2) { - return $this->setIndentation(str_repeat(" ", $iNumber)); - } - - public function nextLevel() { - if($this->oNextLevelFormat === null) { - $this->oNextLevelFormat = clone $this; - $this->oNextLevelFormat->iIndentationLevel++; - $this->oNextLevelFormat->oFormatter = null; - } - return $this->oNextLevelFormat; - } - - public function beLenient() { - $this->bIgnoreExceptions = true; - } - - public function getFormatter() { - if($this->oFormatter === null) { - $this->oFormatter = new OutputFormatter($this); - } - return $this->oFormatter; - } - - public function level() { - return $this->iIndentationLevel; - } + /** + * @var string + */ + public $sSpaceBetweenRules = ''; - /** - * Create format. - * - * @return OutputFormat Format. - */ - public static function create() { - return new OutputFormat(); - } + /** + * @var string + */ + public $sSpaceBeforeBlocks = ''; - /** - * Create compact format. - * - * @return OutputFormat Format. - */ - public static function createCompact() { - $format = self::create(); - $format->set('Space*Rules', "")->set('Space*Blocks', "")->setSpaceAfterRuleName('')->setSpaceBeforeOpeningBrace('')->setSpaceAfterSelectorSeparator(''); - return $format; - } + /** + * @var string + */ + public $sSpaceAfterBlocks = ''; - /** - * Create pretty format. - * - * @return OutputFormat Format. - */ - public static function createPretty() { - $format = self::create(); - $format->set('Space*Rules', "\n")->set('Space*Blocks', "\n")->setSpaceBetweenBlocks("\n\n")->set('SpaceAfterListArgumentSeparator', array('default' => '', ',' => ' ')); - return $format; - } -} - -class OutputFormatter { - private $oFormat; - - public function __construct(OutputFormat $oFormat) { - $this->oFormat = $oFormat; - } - - public function space($sName, $sType = null) { - $sSpaceString = $this->oFormat->get("Space$sName"); - // If $sSpaceString is an array, we have multple values configured depending on the type of object the space applies to - if(is_array($sSpaceString)) { - if($sType !== null && isset($sSpaceString[$sType])) { - $sSpaceString = $sSpaceString[$sType]; - } else { - $sSpaceString = reset($sSpaceString); - } - } - return $this->prepareSpace($sSpaceString); - } - - public function spaceAfterRuleName() { - return $this->space('AfterRuleName'); - } - - public function spaceBeforeRules() { - return $this->space('BeforeRules'); - } - - public function spaceAfterRules() { - return $this->space('AfterRules'); - } - - public function spaceBetweenRules() { - return $this->space('BetweenRules'); - } - - public function spaceBeforeBlocks() { - return $this->space('BeforeBlocks'); - } - - public function spaceAfterBlocks() { - return $this->space('AfterBlocks'); - } - - public function spaceBetweenBlocks() { - return $this->space('BetweenBlocks'); - } - - public function spaceBeforeSelectorSeparator() { - return $this->space('BeforeSelectorSeparator'); - } - - public function spaceAfterSelectorSeparator() { - return $this->space('AfterSelectorSeparator'); - } - - public function spaceBeforeListArgumentSeparator($sSeparator) { - return $this->space('BeforeListArgumentSeparator', $sSeparator); - } - - public function spaceAfterListArgumentSeparator($sSeparator) { - return $this->space('AfterListArgumentSeparator', $sSeparator); - } - - public function spaceBeforeOpeningBrace() { - return $this->space('BeforeOpeningBrace'); - } - - /** - * Runs the given code, either swallowing or passing exceptions, depending on the bIgnoreExceptions setting. - */ - public function safely($cCode) { - if($this->oFormat->get('IgnoreExceptions')) { - // If output exceptions are ignored, run the code with exception guards - try { - return $cCode(); - } catch (OutputException $e) { - return null; - } //Do nothing - } else { - // Run the code as-is - return $cCode(); - } - } - - /** - * Clone of the implode function but calls ->render with the current output format instead of __toString() - */ - public function implode($sSeparator, $aValues, $bIncreaseLevel = false) { - $sResult = ''; - $oFormat = $this->oFormat; - if($bIncreaseLevel) { - $oFormat = $oFormat->nextLevel(); - } - $bIsFirst = true; - foreach($aValues as $mValue) { - if($bIsFirst) { - $bIsFirst = false; - } else { - $sResult .= $sSeparator; - } - if($mValue instanceof \Sabberworm\CSS\Renderable) { - $sResult .= $mValue->render($oFormat); - } else { - $sResult .= $mValue; - } - } - return $sResult; - } - - public function removeLastSemicolon($sString) { - if($this->oFormat->get('SemicolonAfterLastRule')) { - return $sString; - } - $sString = explode(';', $sString); - if(count($sString) < 2) { - return $sString[0]; - } - $sLast = array_pop($sString); - $sNextToLast = array_pop($sString); - array_push($sString, $sNextToLast.$sLast); - return implode(';', $sString); - } - - private function prepareSpace($sSpaceString) { - return str_replace("\n", "\n".$this->indent(), $sSpaceString); - } - - private function indent() { - return str_repeat($this->oFormat->sIndentation, $this->oFormat->level()); - } + /** + * @var string + */ + public $sSpaceBetweenBlocks = "\n"; + + /** + * Content injected in and around at-rule blocks. + * + * @var string + */ + public $sBeforeAtRuleBlock = ''; + + /** + * @var string + */ + public $sAfterAtRuleBlock = ''; + + /** + * This is what’s printed before and after the comma if a declaration block contains multiple selectors. + * + * @var string + */ + public $sSpaceBeforeSelectorSeparator = ''; + + /** + * @var string + */ + public $sSpaceAfterSelectorSeparator = ' '; + + /** + * This is what’s printed after the comma of value lists + * + * @var string + */ + public $sSpaceBeforeListArgumentSeparator = ''; + + /** + * @var string + */ + public $sSpaceAfterListArgumentSeparator = ''; + + /** + * @var string + */ + public $sSpaceBeforeOpeningBrace = ' '; + + /** + * Content injected in and around declaration blocks. + * + * @var string + */ + public $sBeforeDeclarationBlock = ''; + + /** + * @var string + */ + public $sAfterDeclarationBlockSelectors = ''; + + /** + * @var string + */ + public $sAfterDeclarationBlock = ''; + + /** + * Indentation character(s) per level. Only applicable if newlines are used in any of the spacing settings. + * + * @var string + */ + public $sIndentation = "\t"; + + /** + * Output exceptions. + * + * @var bool + */ + public $bIgnoreExceptions = false; + + /** + * @var OutputFormatter|null + */ + private $oFormatter = null; + + /** + * @var OutputFormat|null + */ + private $oNextLevelFormat = null; + + /** + * @var int + */ + private $iIndentationLevel = 0; + + public function __construct() + { + } + + /** + * @param string $sName + * + * @return string|null + */ + public function get($sName) + { + $aVarPrefixes = ['a', 's', 'm', 'b', 'f', 'o', 'c', 'i']; + foreach ($aVarPrefixes as $sPrefix) { + $sFieldName = $sPrefix . ucfirst($sName); + if (isset($this->$sFieldName)) { + return $this->$sFieldName; + } + } + return null; + } + + /** + * @param array|string $aNames + * @param mixed $mValue + * + * @return self|false + */ + public function set($aNames, $mValue) + { + $aVarPrefixes = ['a', 's', 'm', 'b', 'f', 'o', 'c', 'i']; + if (is_string($aNames) && strpos($aNames, '*') !== false) { + $aNames = + [ + str_replace('*', 'Before', $aNames), + str_replace('*', 'Between', $aNames), + str_replace('*', 'After', $aNames), + ]; + } elseif (!is_array($aNames)) { + $aNames = [$aNames]; + } + foreach ($aVarPrefixes as $sPrefix) { + $bDidReplace = false; + foreach ($aNames as $sName) { + $sFieldName = $sPrefix . ucfirst($sName); + if (isset($this->$sFieldName)) { + $this->$sFieldName = $mValue; + $bDidReplace = true; + } + } + if ($bDidReplace) { + return $this; + } + } + // Break the chain so the user knows this option is invalid + return false; + } + + /** + * @param string $sMethodName + * @param array $aArguments + * + * @return mixed + * + * @throws \Exception + */ + public function __call($sMethodName, array $aArguments) + { + if (strpos($sMethodName, 'set') === 0) { + return $this->set(substr($sMethodName, 3), $aArguments[0]); + } elseif (strpos($sMethodName, 'get') === 0) { + return $this->get(substr($sMethodName, 3)); + } elseif (method_exists(OutputFormatter::class, $sMethodName)) { + return call_user_func_array([$this->getFormatter(), $sMethodName], $aArguments); + } else { + throw new \Exception('Unknown OutputFormat method called: ' . $sMethodName); + } + } + + /** + * @param int $iNumber + * + * @return self + */ + public function indentWithTabs($iNumber = 1) + { + return $this->setIndentation(str_repeat("\t", $iNumber)); + } + + /** + * @param int $iNumber + * + * @return self + */ + public function indentWithSpaces($iNumber = 2) + { + return $this->setIndentation(str_repeat(" ", $iNumber)); + } + + /** + * @return OutputFormat + */ + public function nextLevel() + { + if ($this->oNextLevelFormat === null) { + $this->oNextLevelFormat = clone $this; + $this->oNextLevelFormat->iIndentationLevel++; + $this->oNextLevelFormat->oFormatter = null; + } + return $this->oNextLevelFormat; + } + + /** + * @return void + */ + public function beLenient() + { + $this->bIgnoreExceptions = true; + } + + /** + * @return OutputFormatter + */ + public function getFormatter() + { + if ($this->oFormatter === null) { + $this->oFormatter = new OutputFormatter($this); + } + return $this->oFormatter; + } + + /** + * @return int + */ + public function level() + { + return $this->iIndentationLevel; + } + + /** + * Creates an instance of this class without any particular formatting settings. + * + * @return self + */ + public static function create() + { + return new OutputFormat(); + } + + /** + * Creates an instance of this class with a preset for compact formatting. + * + * @return self + */ + public static function createCompact() + { + $format = self::create(); + $format->set('Space*Rules', "")->set('Space*Blocks', "")->setSpaceAfterRuleName('') + ->setSpaceBeforeOpeningBrace('')->setSpaceAfterSelectorSeparator(''); + return $format; + } + + /** + * Creates an instance of this class with a preset for pretty formatting. + * + * @return self + */ + public static function createPretty() + { + $format = self::create(); + $format->set('Space*Rules', "\n")->set('Space*Blocks', "\n") + ->setSpaceBetweenBlocks("\n\n")->set('SpaceAfterListArgumentSeparator', ['default' => '', ',' => ' ']); + return $format; + } } diff --git a/lib/php-css-parser/OutputFormatter.php b/lib/php-css-parser/OutputFormatter.php new file mode 100644 index 00000000000..535feca7ff9 --- /dev/null +++ b/lib/php-css-parser/OutputFormatter.php @@ -0,0 +1,231 @@ +oFormat = $oFormat; + } + + /** + * @param string $sName + * @param string|null $sType + * + * @return string + */ + public function space($sName, $sType = null) + { + $sSpaceString = $this->oFormat->get("Space$sName"); + // If $sSpaceString is an array, we have multiple values configured + // depending on the type of object the space applies to + if (is_array($sSpaceString)) { + if ($sType !== null && isset($sSpaceString[$sType])) { + $sSpaceString = $sSpaceString[$sType]; + } else { + $sSpaceString = reset($sSpaceString); + } + } + return $this->prepareSpace($sSpaceString); + } + + /** + * @return string + */ + public function spaceAfterRuleName() + { + return $this->space('AfterRuleName'); + } + + /** + * @return string + */ + public function spaceBeforeRules() + { + return $this->space('BeforeRules'); + } + + /** + * @return string + */ + public function spaceAfterRules() + { + return $this->space('AfterRules'); + } + + /** + * @return string + */ + public function spaceBetweenRules() + { + return $this->space('BetweenRules'); + } + + /** + * @return string + */ + public function spaceBeforeBlocks() + { + return $this->space('BeforeBlocks'); + } + + /** + * @return string + */ + public function spaceAfterBlocks() + { + return $this->space('AfterBlocks'); + } + + /** + * @return string + */ + public function spaceBetweenBlocks() + { + return $this->space('BetweenBlocks'); + } + + /** + * @return string + */ + public function spaceBeforeSelectorSeparator() + { + return $this->space('BeforeSelectorSeparator'); + } + + /** + * @return string + */ + public function spaceAfterSelectorSeparator() + { + return $this->space('AfterSelectorSeparator'); + } + + /** + * @param string $sSeparator + * + * @return string + */ + public function spaceBeforeListArgumentSeparator($sSeparator) + { + return $this->space('BeforeListArgumentSeparator', $sSeparator); + } + + /** + * @param string $sSeparator + * + * @return string + */ + public function spaceAfterListArgumentSeparator($sSeparator) + { + return $this->space('AfterListArgumentSeparator', $sSeparator); + } + + /** + * @return string + */ + public function spaceBeforeOpeningBrace() + { + return $this->space('BeforeOpeningBrace'); + } + + /** + * Runs the given code, either swallowing or passing exceptions, depending on the `bIgnoreExceptions` setting. + * + * @param string $cCode the name of the function to call + * + * @return string|null + */ + public function safely($cCode) + { + if ($this->oFormat->get('IgnoreExceptions')) { + // If output exceptions are ignored, run the code with exception guards + try { + return $cCode(); + } catch (OutputException $e) { + return null; + } // Do nothing + } else { + // Run the code as-is + return $cCode(); + } + } + + /** + * Clone of the `implode` function, but calls `render` with the current output format instead of `__toString()`. + * + * @param string $sSeparator + * @param array $aValues + * @param bool $bIncreaseLevel + * + * @return string + */ + public function implode($sSeparator, array $aValues, $bIncreaseLevel = false) + { + $sResult = ''; + $oFormat = $this->oFormat; + if ($bIncreaseLevel) { + $oFormat = $oFormat->nextLevel(); + } + $bIsFirst = true; + foreach ($aValues as $mValue) { + if ($bIsFirst) { + $bIsFirst = false; + } else { + $sResult .= $sSeparator; + } + if ($mValue instanceof Renderable) { + $sResult .= $mValue->render($oFormat); + } else { + $sResult .= $mValue; + } + } + return $sResult; + } + + /** + * @param string $sString + * + * @return string + */ + public function removeLastSemicolon($sString) + { + if ($this->oFormat->get('SemicolonAfterLastRule')) { + return $sString; + } + $sString = explode(';', $sString); + if (count($sString) < 2) { + return $sString[0]; + } + $sLast = array_pop($sString); + $sNextToLast = array_pop($sString); + array_push($sString, $sNextToLast . $sLast); + return implode(';', $sString); + } + + /** + * @param string $sSpaceString + * + * @return string + */ + private function prepareSpace($sSpaceString) + { + return str_replace("\n", "\n" . $this->indent(), $sSpaceString); + } + + /** + * @return string + */ + private function indent() + { + return str_repeat($this->oFormat->sIndentation, $this->oFormat->level()); + } +} diff --git a/lib/php-css-parser/Parser.php b/lib/php-css-parser/Parser.php index 2520cb34907..f3b0493a5fd 100644 --- a/lib/php-css-parser/Parser.php +++ b/lib/php-css-parser/Parser.php @@ -4,38 +4,57 @@ namespace Sabberworm\CSS; use Sabberworm\CSS\CSSList\Document; use Sabberworm\CSS\Parsing\ParserState; +use Sabberworm\CSS\Parsing\SourceException; /** - * Parser class parses CSS from text into a data structure. + * This class parses CSS from text into a data structure. */ -class Parser { - private $oParserState; +class Parser +{ + /** + * @var ParserState + */ + private $oParserState; - /** - * Parser constructor. - * Note that that iLineNo starts from 1 and not 0 - * - * @param $sText - * @param Settings|null $oParserSettings - * @param int $iLineNo - */ - public function __construct($sText, Settings $oParserSettings = null, $iLineNo = 1) { - if ($oParserSettings === null) { - $oParserSettings = Settings::create(); - } - $this->oParserState = new ParserState($sText, $oParserSettings, $iLineNo); - } + /** + * @param string $sText + * @param Settings|null $oParserSettings + * @param int $iLineNo the line number (starting from 1, not from 0) + */ + public function __construct($sText, Settings $oParserSettings = null, $iLineNo = 1) + { + if ($oParserSettings === null) { + $oParserSettings = Settings::create(); + } + $this->oParserState = new ParserState($sText, $oParserSettings, $iLineNo); + } - public function setCharset($sCharset) { - $this->oParserState->setCharset($sCharset); - } + /** + * @param string $sCharset + * + * @return void + */ + public function setCharset($sCharset) + { + $this->oParserState->setCharset($sCharset); + } - public function getCharset() { - $this->oParserState->getCharset(); - } - - public function parse() { - return Document::parse($this->oParserState); - } + /** + * @return void + */ + public function getCharset() + { + // Note: The `return` statement is missing here. This is a bug that needs to be fixed. + $this->oParserState->getCharset(); + } + /** + * @return Document + * + * @throws SourceException + */ + public function parse() + { + return Document::parse($this->oParserState); + } } diff --git a/lib/php-css-parser/Parsing/OutputException.php b/lib/php-css-parser/Parsing/OutputException.php index 1c811770430..9bfbc75fb48 100644 --- a/lib/php-css-parser/Parsing/OutputException.php +++ b/lib/php-css-parser/Parsing/OutputException.php @@ -3,10 +3,16 @@ namespace Sabberworm\CSS\Parsing; /** -* Thrown if the CSS parsers attempts to print something invalid -*/ -class OutputException extends SourceException { - public function __construct($sMessage, $iLineNo = 0) { - parent::__construct($sMessage, $iLineNo); - } -} \ No newline at end of file + * 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); + } +} diff --git a/lib/php-css-parser/Parsing/ParserState.php b/lib/php-css-parser/Parsing/ParserState.php index 4305c9a0073..e7d85ee0f1e 100644 --- a/lib/php-css-parser/Parsing/ParserState.php +++ b/lib/php-css-parser/Parsing/ParserState.php @@ -1,310 +1,516 @@ oParserSettings = $oParserSettings; - $this->sText = $sText; - $this->iCurrentPosition = 0; - $this->iLineNo = $iLineNo; - $this->setCharset($this->oParserSettings->sDefaultCharset); - } + /** + * @var array + */ + private $aText; - public function setCharset($sCharset) { - $this->sCharset = $sCharset; - $this->aText = $this->strsplit($this->sText); - $this->iLength = count($this->aText); - } + /** + * @var int + */ + private $iCurrentPosition; - public function getCharset() { - $this->oParserHelper->getCharset(); - return $this->sCharset; - } + /** + * @var string + */ + private $sCharset; - public function currentLine() { - return $this->iLineNo; - } + /** + * @var int + */ + private $iLength; - public function getSettings() { - return $this->oParserSettings; - } + /** + * @var int + */ + private $iLineNo; - public function parseIdentifier($bIgnoreCase = true) { - $sResult = $this->parseCharacter(true); - if ($sResult === null) { - throw new UnexpectedTokenException($sResult, $this->peek(5), 'identifier', $this->iLineNo); - } - $sCharacter = null; - while (($sCharacter = $this->parseCharacter(true)) !== null) { - $sResult .= $sCharacter; - } - if ($bIgnoreCase) { - $sResult = $this->strtolower($sResult); - } - return $sResult; - } + /** + * @param string $sText + * @param int $iLineNo + */ + public function __construct($sText, Settings $oParserSettings, $iLineNo = 1) + { + $this->oParserSettings = $oParserSettings; + $this->sText = $sText; + $this->iCurrentPosition = 0; + $this->iLineNo = $iLineNo; + $this->setCharset($this->oParserSettings->sDefaultCharset); + } - public function parseCharacter($bIsForIdentifier) { - if ($this->peek() === '\\') { - if ($bIsForIdentifier && $this->oParserSettings->bLenientParsing && ($this->comes('\0') || $this->comes('\9'))) { - // Non-strings can contain \0 or \9 which is an IE hack supported in lenient parsing. - return null; - } - $this->consume('\\'); - if ($this->comes('\n') || $this->comes('\r')) { - return ''; - } - if (preg_match('/[0-9a-fA-F]/Su', $this->peek()) === 0) { - return $this->consume(1); - } - $sUnicode = $this->consumeExpression('/^[0-9a-fA-F]{1,6}/u', 6); - if ($this->strlen($sUnicode) < 6) { - //Consume whitespace after incomplete unicode escape - if (preg_match('/\\s/isSu', $this->peek())) { - if ($this->comes('\r\n')) { - $this->consume(2); - } else { - $this->consume(1); - } - } - } - $iUnicode = intval($sUnicode, 16); - $sUtf32 = ""; - for ($i = 0; $i < 4; ++$i) { - $sUtf32 .= chr($iUnicode & 0xff); - $iUnicode = $iUnicode >> 8; - } - return iconv('utf-32le', $this->sCharset, $sUtf32); - } - if ($bIsForIdentifier) { - $peek = ord($this->peek()); - // Ranges: a-z A-Z 0-9 - _ - if (($peek >= 97 && $peek <= 122) || - ($peek >= 65 && $peek <= 90) || - ($peek >= 48 && $peek <= 57) || - ($peek === 45) || - ($peek === 95) || - ($peek > 0xa1)) { - return $this->consume(1); - } - } else { - return $this->consume(1); - } - return null; - } + /** + * @param string $sCharset + * + * @return void + */ + public function setCharset($sCharset) + { + $this->sCharset = $sCharset; + $this->aText = $this->strsplit($this->sText); + if (is_array($this->aText)) { + $this->iLength = count($this->aText); + } + } - public function consumeWhiteSpace() { - $comments = array(); - do { - while (preg_match('/\\s/isSu', $this->peek()) === 1) { - $this->consume(1); - } - if($this->oParserSettings->bLenientParsing) { - try { - $oComment = $this->consumeComment(); - } catch(UnexpectedTokenException $e) { - // When we can’t find the end of a comment, we assume the document is finished. - $this->iCurrentPosition = $this->iLength; - return; - } - } else { - $oComment = $this->consumeComment(); - } - if ($oComment !== false) { - $comments[] = $oComment; - } - } while($oComment !== false); - return $comments; - } + /** + * @return string + */ + public function getCharset() + { + return $this->sCharset; + } - public function comes($sString, $bCaseInsensitive = false) { - $sPeek = $this->peek(strlen($sString)); - return ($sPeek == '') - ? false - : $this->streql($sPeek, $sString, $bCaseInsensitive); - } + /** + * @return int + */ + public function currentLine() + { + return $this->iLineNo; + } - public function peek($iLength = 1, $iOffset = 0) { - $iOffset += $this->iCurrentPosition; - if ($iOffset >= $this->iLength) { - return ''; - } - return $this->substr($iOffset, $iLength); - } + /** + * @return int + */ + public function currentColumn() + { + return $this->iCurrentPosition; + } - public function consume($mValue = 1) { - if (is_string($mValue)) { - $iLineCount = substr_count($mValue, "\n"); - $iLength = $this->strlen($mValue); - if (!$this->streql($this->substr($this->iCurrentPosition, $iLength), $mValue)) { - throw new UnexpectedTokenException($mValue, $this->peek(max($iLength, 5)), $this->iLineNo); - } - $this->iLineNo += $iLineCount; - $this->iCurrentPosition += $this->strlen($mValue); - return $mValue; - } else { - if ($this->iCurrentPosition + $mValue > $this->iLength) { - throw new UnexpectedTokenException($mValue, $this->peek(5), 'count', $this->iLineNo); - } - $sResult = $this->substr($this->iCurrentPosition, $mValue); - $iLineCount = substr_count($sResult, "\n"); - $this->iLineNo += $iLineCount; - $this->iCurrentPosition += $mValue; - return $sResult; - } - } + /** + * @return Settings + */ + public function getSettings() + { + return $this->oParserSettings; + } - public function consumeExpression($mExpression, $iMaxLength = null) { - $aMatches = null; - $sInput = $iMaxLength !== null ? $this->peek($iMaxLength) : $this->inputLeft(); - if (preg_match($mExpression, $sInput, $aMatches, PREG_OFFSET_CAPTURE) === 1) { - return $this->consume($aMatches[0][0]); - } - throw new UnexpectedTokenException($mExpression, $this->peek(5), 'expression', $this->iLineNo); - } + /** + * @param bool $bIgnoreCase + * + * @return string + * + * @throws UnexpectedTokenException + */ + public function parseIdentifier($bIgnoreCase = true) + { + $sResult = $this->parseCharacter(true); + if ($sResult === null) { + throw new UnexpectedTokenException($sResult, $this->peek(5), 'identifier', $this->iLineNo); + } + $sCharacter = null; + while (($sCharacter = $this->parseCharacter(true)) !== null) { + if (preg_match('/[a-zA-Z0-9\x{00A0}-\x{FFFF}_-]/Sux', $sCharacter)) { + $sResult .= $sCharacter; + } else { + $sResult .= '\\' . $sCharacter; + } + } + if ($bIgnoreCase) { + $sResult = $this->strtolower($sResult); + } + return $sResult; + } - /** - * @return false|Comment - */ - public function consumeComment() { - $mComment = false; - if ($this->comes('/*')) { - $iLineNo = $this->iLineNo; - $this->consume(1); - $mComment = ''; - while (($char = $this->consume(1)) !== '') { - $mComment .= $char; - if ($this->comes('*/')) { - $this->consume(2); - break; - } - } - } + /** + * @param bool $bIsForIdentifier + * + * @return string|null + * + * @throws UnexpectedEOFException + * @throws UnexpectedTokenException + */ + public function parseCharacter($bIsForIdentifier) + { + if ($this->peek() === '\\') { + if ( + $bIsForIdentifier && $this->oParserSettings->bLenientParsing + && ($this->comes('\0') || $this->comes('\9')) + ) { + // Non-strings can contain \0 or \9 which is an IE hack supported in lenient parsing. + return null; + } + $this->consume('\\'); + if ($this->comes('\n') || $this->comes('\r')) { + return ''; + } + if (preg_match('/[0-9a-fA-F]/Su', $this->peek()) === 0) { + return $this->consume(1); + } + $sUnicode = $this->consumeExpression('/^[0-9a-fA-F]{1,6}/u', 6); + if ($this->strlen($sUnicode) < 6) { + // Consume whitespace after incomplete unicode escape + if (preg_match('/\\s/isSu', $this->peek())) { + if ($this->comes('\r\n')) { + $this->consume(2); + } else { + $this->consume(1); + } + } + } + $iUnicode = intval($sUnicode, 16); + $sUtf32 = ""; + for ($i = 0; $i < 4; ++$i) { + $sUtf32 .= chr($iUnicode & 0xff); + $iUnicode = $iUnicode >> 8; + } + return iconv('utf-32le', $this->sCharset, $sUtf32); + } + if ($bIsForIdentifier) { + $peek = ord($this->peek()); + // Ranges: a-z A-Z 0-9 - _ + if ( + ($peek >= 97 && $peek <= 122) + || ($peek >= 65 && $peek <= 90) + || ($peek >= 48 && $peek <= 57) + || ($peek === 45) + || ($peek === 95) + || ($peek > 0xa1) + ) { + return $this->consume(1); + } + } else { + return $this->consume(1); + } + return null; + } - if ($mComment !== false) { - // We skip the * which was included in the comment. - return new Comment(substr($mComment, 1), $iLineNo); - } + /** + * @return array|void + * + * @throws UnexpectedEOFException + * @throws UnexpectedTokenException + */ + public function consumeWhiteSpace() + { + $comments = []; + do { + while (preg_match('/\\s/isSu', $this->peek()) === 1) { + $this->consume(1); + } + if ($this->oParserSettings->bLenientParsing) { + try { + $oComment = $this->consumeComment(); + } catch (UnexpectedEOFException $e) { + $this->iCurrentPosition = $this->iLength; + return; + } + } else { + $oComment = $this->consumeComment(); + } + if ($oComment !== false) { + $comments[] = $oComment; + } + } while ($oComment !== false); + return $comments; + } - return $mComment; - } + /** + * @param string $sString + * @param bool $bCaseInsensitive + * + * @return bool + */ + public function comes($sString, $bCaseInsensitive = false) + { + $sPeek = $this->peek(strlen($sString)); + return ($sPeek == '') + ? false + : $this->streql($sPeek, $sString, $bCaseInsensitive); + } - public function isEnd() { - return $this->iCurrentPosition >= $this->iLength; - } + /** + * @param int $iLength + * @param int $iOffset + * + * @return string + */ + public function peek($iLength = 1, $iOffset = 0) + { + $iOffset += $this->iCurrentPosition; + if ($iOffset >= $this->iLength) { + return ''; + } + return $this->substr($iOffset, $iLength); + } - public function consumeUntil($aEnd, $bIncludeEnd = false, $consumeEnd = false, array &$comments = array()) { - $aEnd = is_array($aEnd) ? $aEnd : array($aEnd); - $out = ''; - $start = $this->iCurrentPosition; + /** + * @param int $mValue + * + * @return string + * + * @throws UnexpectedEOFException + * @throws UnexpectedTokenException + */ + public function consume($mValue = 1) + { + if (is_string($mValue)) { + $iLineCount = substr_count($mValue, "\n"); + $iLength = $this->strlen($mValue); + if (!$this->streql($this->substr($this->iCurrentPosition, $iLength), $mValue)) { + throw new UnexpectedTokenException($mValue, $this->peek(max($iLength, 5)), $this->iLineNo); + } + $this->iLineNo += $iLineCount; + $this->iCurrentPosition += $this->strlen($mValue); + return $mValue; + } else { + if ($this->iCurrentPosition + $mValue > $this->iLength) { + throw new UnexpectedEOFException($mValue, $this->peek(5), 'count', $this->iLineNo); + } + $sResult = $this->substr($this->iCurrentPosition, $mValue); + $iLineCount = substr_count($sResult, "\n"); + $this->iLineNo += $iLineCount; + $this->iCurrentPosition += $mValue; + return $sResult; + } + } - while (($char = $this->consume(1)) !== '') { - if (in_array($char, $aEnd)) { - if ($bIncludeEnd) { - $out .= $char; - } elseif (!$consumeEnd) { - $this->iCurrentPosition -= $this->strlen($char); - } - return $out; - } - $out .= $char; - if ($comment = $this->consumeComment()) { - $comments[] = $comment; - } - } + /** + * @param string $mExpression + * @param int|null $iMaxLength + * + * @return string + * + * @throws UnexpectedEOFException + * @throws UnexpectedTokenException + */ + public function consumeExpression($mExpression, $iMaxLength = null) + { + $aMatches = null; + $sInput = $iMaxLength !== null ? $this->peek($iMaxLength) : $this->inputLeft(); + if (preg_match($mExpression, $sInput, $aMatches, PREG_OFFSET_CAPTURE) === 1) { + return $this->consume($aMatches[0][0]); + } + throw new UnexpectedTokenException($mExpression, $this->peek(5), 'expression', $this->iLineNo); + } - $this->iCurrentPosition = $start; - throw new UnexpectedTokenException('One of ("'.implode('","', $aEnd).'")', $this->peek(5), 'search', $this->iLineNo); - } + /** + * @return Comment|false + */ + public function consumeComment() + { + $mComment = false; + if ($this->comes('/*')) { + $iLineNo = $this->iLineNo; + $this->consume(1); + $mComment = ''; + while (($char = $this->consume(1)) !== '') { + $mComment .= $char; + if ($this->comes('*/')) { + $this->consume(2); + break; + } + } + } - private function inputLeft() { - return $this->substr($this->iCurrentPosition, -1); - } + if ($mComment !== false) { + // We skip the * which was included in the comment. + return new Comment(substr($mComment, 1), $iLineNo); + } - public function streql($sString1, $sString2, $bCaseInsensitive = true) { - if($bCaseInsensitive) { - return $this->strtolower($sString1) === $this->strtolower($sString2); - } else { - return $sString1 === $sString2; - } - } + return $mComment; + } - public function backtrack($iAmount) { - $this->iCurrentPosition -= $iAmount; - } + /** + * @return bool + */ + public function isEnd() + { + return $this->iCurrentPosition >= $this->iLength; + } - public function strlen($sString) { - if ($this->oParserSettings->bMultibyteSupport) { - return mb_strlen($sString, $this->sCharset); - } else { - return strlen($sString); - } - } + /** + * @param array|string $aEnd + * @param string $bIncludeEnd + * @param string $consumeEnd + * @param array $comments + * + * @return string + * + * @throws UnexpectedEOFException + * @throws UnexpectedTokenException + */ + public function consumeUntil($aEnd, $bIncludeEnd = false, $consumeEnd = false, array &$comments = []) + { + $aEnd = is_array($aEnd) ? $aEnd : [$aEnd]; + $out = ''; + $start = $this->iCurrentPosition; - private function substr($iStart, $iLength) { - if ($iLength < 0) { - $iLength = $this->iLength - $iStart + $iLength; - } - if ($iStart + $iLength > $this->iLength) { - $iLength = $this->iLength - $iStart; - } - $sResult = ''; - while ($iLength > 0) { - $sResult .= $this->aText[$iStart]; - $iStart++; - $iLength--; - } - return $sResult; - } + while (!$this->isEnd()) { + $char = $this->consume(1); + if (in_array($char, $aEnd)) { + if ($bIncludeEnd) { + $out .= $char; + } elseif (!$consumeEnd) { + $this->iCurrentPosition -= $this->strlen($char); + } + return $out; + } + $out .= $char; + if ($comment = $this->consumeComment()) { + $comments[] = $comment; + } + } - private function strtolower($sString) { - if ($this->oParserSettings->bMultibyteSupport) { - return mb_strtolower($sString, $this->sCharset); - } else { - return strtolower($sString); - } - } + if (in_array(self::EOF, $aEnd)) { + return $out; + } - private function strsplit($sString) { - if ($this->oParserSettings->bMultibyteSupport) { - if ($this->streql($this->sCharset, 'utf-8')) { - return preg_split('//u', $sString, null, PREG_SPLIT_NO_EMPTY); - } else { - $iLength = mb_strlen($sString, $this->sCharset); - $aResult = array(); - for ($i = 0; $i < $iLength; ++$i) { - $aResult[] = mb_substr($sString, $i, 1, $this->sCharset); - } - return $aResult; - } - } else { - if($sString === '') { - return array(); - } else { - return str_split($sString); - } - } - } + $this->iCurrentPosition = $start; + throw new UnexpectedEOFException( + 'One of ("' . implode('","', $aEnd) . '")', + $this->peek(5), + 'search', + $this->iLineNo + ); + } - private function strpos($sString, $sNeedle, $iOffset) { - if ($this->oParserSettings->bMultibyteSupport) { - return mb_strpos($sString, $sNeedle, $iOffset, $this->sCharset); - } else { - return strpos($sString, $sNeedle, $iOffset); - } - } -} \ No newline at end of file + /** + * @return string + */ + private function inputLeft() + { + return $this->substr($this->iCurrentPosition, -1); + } + + /** + * @param string $sString1 + * @param string $sString2 + * @param bool $bCaseInsensitive + * + * @return bool + */ + public function streql($sString1, $sString2, $bCaseInsensitive = true) + { + if ($bCaseInsensitive) { + return $this->strtolower($sString1) === $this->strtolower($sString2); + } else { + return $sString1 === $sString2; + } + } + + /** + * @param int $iAmount + * + * @return void + */ + public function backtrack($iAmount) + { + $this->iCurrentPosition -= $iAmount; + } + + /** + * @param string $sString + * + * @return int + */ + public function strlen($sString) + { + if ($this->oParserSettings->bMultibyteSupport) { + return mb_strlen($sString, $this->sCharset); + } else { + return strlen($sString); + } + } + + /** + * @param int $iStart + * @param int $iLength + * + * @return string + */ + private function substr($iStart, $iLength) + { + if ($iLength < 0) { + $iLength = $this->iLength - $iStart + $iLength; + } + if ($iStart + $iLength > $this->iLength) { + $iLength = $this->iLength - $iStart; + } + $sResult = ''; + while ($iLength > 0) { + $sResult .= $this->aText[$iStart]; + $iStart++; + $iLength--; + } + return $sResult; + } + + /** + * @param string $sString + * + * @return string + */ + private function strtolower($sString) + { + if ($this->oParserSettings->bMultibyteSupport) { + return mb_strtolower($sString, $this->sCharset); + } else { + return strtolower($sString); + } + } + + /** + * @param string $sString + * + * @return array + */ + private function strsplit($sString) + { + if ($this->oParserSettings->bMultibyteSupport) { + if ($this->streql($this->sCharset, 'utf-8')) { + return preg_split('//u', $sString, -1, PREG_SPLIT_NO_EMPTY); + } else { + $iLength = mb_strlen($sString, $this->sCharset); + $aResult = []; + for ($i = 0; $i < $iLength; ++$i) { + $aResult[] = mb_substr($sString, $i, 1, $this->sCharset); + } + return $aResult; + } + } else { + if ($sString === '') { + return []; + } else { + return str_split($sString); + } + } + } + + /** + * @param string $sString + * @param string $sNeedle + * @param int $iOffset + * + * @return int|false + */ + private function strpos($sString, $sNeedle, $iOffset) + { + if ($this->oParserSettings->bMultibyteSupport) { + return mb_strpos($sString, $sNeedle, $iOffset, $this->sCharset); + } else { + return strpos($sString, $sNeedle, $iOffset); + } + } +} diff --git a/lib/php-css-parser/Parsing/SourceException.php b/lib/php-css-parser/Parsing/SourceException.php index 9bb9913847f..1ca668a990e 100644 --- a/lib/php-css-parser/Parsing/SourceException.php +++ b/lib/php-css-parser/Parsing/SourceException.php @@ -2,17 +2,31 @@ namespace Sabberworm\CSS\Parsing; -class SourceException extends \Exception { - private $iLineNo; - public function __construct($sMessage, $iLineNo = 0) { - $this->iLineNo = $iLineNo; - if (!empty($iLineNo)) { - $sMessage .= " [line no: $iLineNo]"; - } - parent::__construct($sMessage); - } +class SourceException extends \Exception +{ + /** + * @var int + */ + private $iLineNo; - public function getLineNo() { - return $this->iLineNo; - } -} \ No newline at end of file + /** + * @param string $sMessage + * @param int $iLineNo + */ + public function __construct($sMessage, $iLineNo = 0) + { + $this->iLineNo = $iLineNo; + if (!empty($iLineNo)) { + $sMessage .= " [line no: $iLineNo]"; + } + parent::__construct($sMessage); + } + + /** + * @return int + */ + public function getLineNo() + { + return $this->iLineNo; + } +} diff --git a/lib/php-css-parser/Parsing/UnexpectedEOFException.php b/lib/php-css-parser/Parsing/UnexpectedEOFException.php new file mode 100644 index 00000000000..368ec70c74e --- /dev/null +++ b/lib/php-css-parser/Parsing/UnexpectedEOFException.php @@ -0,0 +1,12 @@ +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}”."; - } else if($this->sMatchType === 'count') { - $sMessage = "Next token was expected to have {$sExpected} chars. Context: “{$sFound}”."; - } else if($this->sMatchType === 'identifier') { - $sMessage = "Identifier expected. Got “{$sFound}”"; - } else if($this->sMatchType === 'custom') { - $sMessage = trim("$sExpected $sFound"); - } + /** + * @var string + */ + private $sFound; - parent::__construct($sMessage, $iLineNo); - } -} \ No newline at end of file + /** + * Possible values: literal, identifier, count, expression, search + * + * @var string + */ + private $sMatchType; + + /** + * @param string $sExpected + * @param string $sFound + * @param string $sMatchType + * @param int $iLineNo + */ + public function __construct($sExpected, $sFound, $sMatchType = 'literal', $iLineNo = 0) + { + $this->sExpected = $sExpected; + $this->sFound = $sFound; + $this->sMatchType = $sMatchType; + $sMessage = "Token “{$sExpected}” ({$sMatchType}) not found. Got “{$sFound}”."; + if ($this->sMatchType === 'search') { + $sMessage = "Search for “{$sExpected}” returned no results. Context: “{$sFound}”."; + } elseif ($this->sMatchType === 'count') { + $sMessage = "Next token was expected to have {$sExpected} chars. Context: “{$sFound}”."; + } elseif ($this->sMatchType === 'identifier') { + $sMessage = "Identifier expected. Got “{$sFound}”"; + } elseif ($this->sMatchType === 'custom') { + $sMessage = trim("$sExpected $sFound"); + } + + parent::__construct($sMessage, $iLineNo); + } +} diff --git a/lib/php-css-parser/Property/AtRule.php b/lib/php-css-parser/Property/AtRule.php index b20c8c6e273..9536ff5e973 100644 --- a/lib/php-css-parser/Property/AtRule.php +++ b/lib/php-css-parser/Property/AtRule.php @@ -2,15 +2,33 @@ namespace Sabberworm\CSS\Property; -use Sabberworm\CSS\Renderable; use Sabberworm\CSS\Comment\Commentable; +use Sabberworm\CSS\Renderable; -interface AtRule extends Renderable, Commentable { - // Since there are more set rules than block rules, we’re whitelisting the block rules and have anything else be treated as a set rule. - const BLOCK_RULES = 'media/document/supports/region-style/font-feature-values'; - // …and more font-specific ones (to be used inside font-feature-values) - const SET_RULES = 'font-face/counter-style/page/swash/styleset/annotation'; - - public function atRuleName(); - public function atRuleArgs(); -} \ No newline at end of file +interface AtRule extends Renderable, Commentable +{ + /** + * Since there are more set rules than block rules, + * we’re whitelisting the block rules and have anything else be treated as a set rule. + * + * @var string + */ + const BLOCK_RULES = 'media/document/supports/region-style/font-feature-values'; + + /** + * … and more font-specific ones (to be used inside font-feature-values) + * + * @var string + */ + const SET_RULES = 'font-face/counter-style/page/swash/styleset/annotation'; + + /** + * @return string|null + */ + public function atRuleName(); + + /** + * @return string|null + */ + public function atRuleArgs(); +} diff --git a/lib/php-css-parser/Property/CSSNamespace.php b/lib/php-css-parser/Property/CSSNamespace.php index 7c0dee15981..0d7eb496967 100644 --- a/lib/php-css-parser/Property/CSSNamespace.php +++ b/lib/php-css-parser/Property/CSSNamespace.php @@ -2,74 +2,153 @@ namespace Sabberworm\CSS\Property; +use Sabberworm\CSS\Comment\Comment; +use Sabberworm\CSS\OutputFormat; + /** -* CSSNamespace represents an @namespace rule. -*/ -class CSSNamespace implements AtRule { - private $mUrl; - private $sPrefix; - private $iLineNo; - protected $aComments; - - public function __construct($mUrl, $sPrefix = null, $iLineNo = 0) { - $this->mUrl = $mUrl; - $this->sPrefix = $sPrefix; - $this->iLineNo = $iLineNo; - $this->aComments = array(); - } + * `CSSNamespace` represents an `@namespace` rule. + */ +class CSSNamespace implements AtRule +{ + /** + * @var string + */ + private $mUrl; - /** - * @return int - */ - public function getLineNo() { - return $this->iLineNo; - } + /** + * @var string + */ + private $sPrefix; - public function __toString() { - return $this->render(new \Sabberworm\CSS\OutputFormat()); - } + /** + * @var int + */ + private $iLineNo; - public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) { - return '@namespace '.($this->sPrefix === null ? '' : $this->sPrefix.' ').$this->mUrl->render($oOutputFormat).';'; - } - - public function getUrl() { - return $this->mUrl; - } + /** + * @var array + */ + protected $aComments; - public function getPrefix() { - return $this->sPrefix; - } + /** + * @param string $mUrl + * @param string|null $sPrefix + * @param int $iLineNo + */ + public function __construct($mUrl, $sPrefix = null, $iLineNo = 0) + { + $this->mUrl = $mUrl; + $this->sPrefix = $sPrefix; + $this->iLineNo = $iLineNo; + $this->aComments = []; + } - public function setUrl($mUrl) { - $this->mUrl = $mUrl; - } + /** + * @return int + */ + public function getLineNo() + { + return $this->iLineNo; + } - public function setPrefix($sPrefix) { - $this->sPrefix = $sPrefix; - } + /** + * @return string + */ + public function __toString() + { + return $this->render(new OutputFormat()); + } - public function atRuleName() { - return 'namespace'; - } + /** + * @return string + */ + public function render(OutputFormat $oOutputFormat) + { + return '@namespace ' . ($this->sPrefix === null ? '' : $this->sPrefix . ' ') + . $this->mUrl->render($oOutputFormat) . ';'; + } - public function atRuleArgs() { - $aResult = array($this->mUrl); - if($this->sPrefix) { - array_unshift($aResult, $this->sPrefix); - } - return $aResult; - } + /** + * @return string + */ + public function getUrl() + { + return $this->mUrl; + } - public function addComments(array $aComments) { - $this->aComments = array_merge($this->aComments, $aComments); - } + /** + * @return string|null + */ + public function getPrefix() + { + return $this->sPrefix; + } - public function getComments() { - return $this->aComments; - } + /** + * @param string $mUrl + * + * @return void + */ + public function setUrl($mUrl) + { + $this->mUrl = $mUrl; + } - public function setComments(array $aComments) { - $this->aComments = $aComments; - } -} \ No newline at end of file + /** + * @param string $sPrefix + * + * @return void + */ + public function setPrefix($sPrefix) + { + $this->sPrefix = $sPrefix; + } + + /** + * @return string + */ + public function atRuleName() + { + return 'namespace'; + } + + /** + * @return array + */ + public function atRuleArgs() + { + $aResult = [$this->mUrl]; + if ($this->sPrefix) { + array_unshift($aResult, $this->sPrefix); + } + return $aResult; + } + + /** + * @param array $aComments + * + * @return void + */ + public function addComments(array $aComments) + { + $this->aComments = array_merge($this->aComments, $aComments); + } + + /** + * @return array + */ + public function getComments() + { + return $this->aComments; + } + + /** + * @param array $aComments + * + * @return void + */ + public function setComments(array $aComments) + { + $this->aComments = $aComments; + } +} diff --git a/lib/php-css-parser/Property/Charset.php b/lib/php-css-parser/Property/Charset.php index 61c6ebc5b63..3ee0c3d042e 100644 --- a/lib/php-css-parser/Property/Charset.php +++ b/lib/php-css-parser/Property/Charset.php @@ -2,65 +2,128 @@ namespace Sabberworm\CSS\Property; +use Sabberworm\CSS\Comment\Comment; +use Sabberworm\CSS\OutputFormat; + /** - * Class representing an @charset rule. + * Class representing an `@charset` rule. + * * The following restrictions apply: - * • May not be found in any CSSList other than the Document. - * • May only appear at the very top of a Document’s contents. - * • Must not appear more than once. + * - May not be found in any CSSList other than the Document. + * - May only appear at the very top of a Document’s contents. + * - Must not appear more than once. */ -class Charset implements AtRule { +class Charset implements AtRule +{ + /** + * @var string + */ + private $sCharset; - private $sCharset; - protected $iLineNo; - protected $aComment; + /** + * @var int + */ + protected $iLineNo; - public function __construct($sCharset, $iLineNo = 0) { - $this->sCharset = $sCharset; - $this->iLineNo = $iLineNo; - $this->aComments = array(); - } + /** + * @var array + */ + protected $aComments; - /** - * @return int - */ - public function getLineNo() { - return $this->iLineNo; - } + /** + * @param string $sCharset + * @param int $iLineNo + */ + public function __construct($sCharset, $iLineNo = 0) + { + $this->sCharset = $sCharset; + $this->iLineNo = $iLineNo; + $this->aComments = []; + } - public function setCharset($sCharset) { - $this->sCharset = $sCharset; - } + /** + * @return int + */ + public function getLineNo() + { + return $this->iLineNo; + } - public function getCharset() { - return $this->sCharset; - } + /** + * @param string $sCharset + * + * @return void + */ + public function setCharset($sCharset) + { + $this->sCharset = $sCharset; + } - public function __toString() { - return $this->render(new \Sabberworm\CSS\OutputFormat()); - } + /** + * @return string + */ + public function getCharset() + { + return $this->sCharset; + } - public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) { - return "@charset {$this->sCharset->render($oOutputFormat)};"; - } + /** + * @return string + */ + public function __toString() + { + return $this->render(new OutputFormat()); + } - public function atRuleName() { - return 'charset'; - } + /** + * @return string + */ + public function render(OutputFormat $oOutputFormat) + { + return "@charset {$this->sCharset->render($oOutputFormat)};"; + } - public function atRuleArgs() { - return $this->sCharset; - } + /** + * @return string + */ + public function atRuleName() + { + return 'charset'; + } - public function addComments(array $aComments) { - $this->aComments = array_merge($this->aComments, $aComments); - } + /** + * @return string + */ + public function atRuleArgs() + { + return $this->sCharset; + } - public function getComments() { - return $this->aComments; - } + /** + * @param array $aComments + * + * @return void + */ + public function addComments(array $aComments) + { + $this->aComments = array_merge($this->aComments, $aComments); + } - public function setComments(array $aComments) { - $this->aComments = $aComments; - } -} \ No newline at end of file + /** + * @return array + */ + public function getComments() + { + return $this->aComments; + } + + /** + * @param array $aComments + * + * @return void + */ + public function setComments(array $aComments) + { + $this->aComments = $aComments; + } +} diff --git a/lib/php-css-parser/Property/Import.php b/lib/php-css-parser/Property/Import.php index 4d40f5a04ca..a2253016b0c 100644 --- a/lib/php-css-parser/Property/Import.php +++ b/lib/php-css-parser/Property/Import.php @@ -2,68 +2,136 @@ namespace Sabberworm\CSS\Property; +use Sabberworm\CSS\Comment\Comment; +use Sabberworm\CSS\OutputFormat; use Sabberworm\CSS\Value\URL; /** -* Class representing an @import rule. -*/ -class Import implements AtRule { - private $oLocation; - private $sMediaQuery; - protected $iLineNo; - protected $aComments; - - public function __construct(URL $oLocation, $sMediaQuery, $iLineNo = 0) { - $this->oLocation = $oLocation; - $this->sMediaQuery = $sMediaQuery; - $this->iLineNo = $iLineNo; - $this->aComments = array(); - } + * Class representing an `@import` rule. + */ +class Import implements AtRule +{ + /** + * @var URL + */ + private $oLocation; - /** - * @return int - */ - public function getLineNo() { - return $this->iLineNo; - } + /** + * @var string + */ + private $sMediaQuery; - public function setLocation($oLocation) { - $this->oLocation = $oLocation; - } + /** + * @var int + */ + protected $iLineNo; - public function getLocation() { - return $this->oLocation; - } - - public function __toString() { - return $this->render(new \Sabberworm\CSS\OutputFormat()); - } + /** + * @var array + */ + protected $aComments; - public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) { - return "@import ".$this->oLocation->render($oOutputFormat).($this->sMediaQuery === null ? '' : ' '.$this->sMediaQuery).';'; - } + /** + * @param URL $oLocation + * @param string $sMediaQuery + * @param int $iLineNo + */ + public function __construct(URL $oLocation, $sMediaQuery, $iLineNo = 0) + { + $this->oLocation = $oLocation; + $this->sMediaQuery = $sMediaQuery; + $this->iLineNo = $iLineNo; + $this->aComments = []; + } - public function atRuleName() { - return 'import'; - } + /** + * @return int + */ + public function getLineNo() + { + return $this->iLineNo; + } - public function atRuleArgs() { - $aResult = array($this->oLocation); - if($this->sMediaQuery) { - array_push($aResult, $this->sMediaQuery); - } - return $aResult; - } + /** + * @param URL $oLocation + * + * @return void + */ + public function setLocation($oLocation) + { + $this->oLocation = $oLocation; + } - public function addComments(array $aComments) { - $this->aComments = array_merge($this->aComments, $aComments); - } + /** + * @return URL + */ + public function getLocation() + { + return $this->oLocation; + } - public function getComments() { - return $this->aComments; - } + /** + * @return string + */ + public function __toString() + { + return $this->render(new OutputFormat()); + } - public function setComments(array $aComments) { - $this->aComments = $aComments; - } -} \ No newline at end of file + /** + * @return string + */ + public function render(OutputFormat $oOutputFormat) + { + return "@import " . $this->oLocation->render($oOutputFormat) + . ($this->sMediaQuery === null ? '' : ' ' . $this->sMediaQuery) . ';'; + } + + /** + * @return string + */ + public function atRuleName() + { + return 'import'; + } + + /** + * @return array + */ + public function atRuleArgs() + { + $aResult = [$this->oLocation]; + if ($this->sMediaQuery) { + array_push($aResult, $this->sMediaQuery); + } + return $aResult; + } + + /** + * @param array $aComments + * + * @return void + */ + public function addComments(array $aComments) + { + $this->aComments = array_merge($this->aComments, $aComments); + } + + /** + * @return array + */ + public function getComments() + { + return $this->aComments; + } + + /** + * @param array $aComments + * + * @return void + */ + public function setComments(array $aComments) + { + $this->aComments = $aComments; + } +} diff --git a/lib/php-css-parser/Property/KeyframeSelector.php b/lib/php-css-parser/Property/KeyframeSelector.php new file mode 100644 index 00000000000..14ea5ebb77a --- /dev/null +++ b/lib/php-css-parser/Property/KeyframeSelector.php @@ -0,0 +1,23 @@ +]* # any sequence of valid unescaped characters + (?:\\\\.)? # a single escaped character + (?:([\'"]).*?(?\~]+)[\w]+ # elements + | + \:{1,2}( # pseudo-elements + after|before|first-letter|first-line|selection + )) + /ix'; - const ELEMENTS_AND_PSEUDO_ELEMENTS_RX = '/ - ((^|[\s\+\>\~]+)[\w]+ # elements - | - \:{1,2}( # pseudo-elements - after|before|first-letter|first-line|selection - )) - /ix'; + /** + * regexp for specificity calculations + * + * @var string + */ + const SELECTOR_VALIDATION_RX = '/ + ^( + (?: + [a-zA-Z0-9\x{00A0}-\x{FFFF}_^$|*="\'~\[\]()\-\s\.:#+>]* # any sequence of valid unescaped characters + (?:\\\\.)? # a single escaped character + (?:([\'"]).*?(?setSelector($sSelector); - if ($bCalculateSpecificity) { - $this->getSpecificity(); - } - } + /** + * @var int|null + */ + private $iSpecificity; - public function getSelector() { - return $this->sSelector; - } + /** + * @param string $sSelector + * + * @return bool + */ + public static function isValid($sSelector) + { + return preg_match(static::SELECTOR_VALIDATION_RX, $sSelector); + } - public function setSelector($sSelector) { - $this->sSelector = trim($sSelector); - $this->iSpecificity = null; - } + /** + * @param string $sSelector + * @param bool $bCalculateSpecificity + */ + public function __construct($sSelector, $bCalculateSpecificity = false) + { + $this->setSelector($sSelector); + if ($bCalculateSpecificity) { + $this->getSpecificity(); + } + } - public function __toString() { - return $this->getSelector(); - } + /** + * @return string + */ + public function getSelector() + { + return $this->sSelector; + } - public function getSpecificity() { - if ($this->iSpecificity === null) { - $a = 0; - /// @todo should exclude \# as well as "#" - $aMatches = null; - $b = substr_count($this->sSelector, '#'); - $c = preg_match_all(self::NON_ID_ATTRIBUTES_AND_PSEUDO_CLASSES_RX, $this->sSelector, $aMatches); - $d = preg_match_all(self::ELEMENTS_AND_PSEUDO_ELEMENTS_RX, $this->sSelector, $aMatches); - $this->iSpecificity = ($a * 1000) + ($b * 100) + ($c * 10) + $d; - } - return $this->iSpecificity; - } + /** + * @param string $sSelector + * + * @return void + */ + public function setSelector($sSelector) + { + $this->sSelector = trim($sSelector); + $this->iSpecificity = null; + } + /** + * @return string + */ + public function __toString() + { + return $this->getSelector(); + } + + /** + * @return int + */ + public function getSpecificity() + { + if ($this->iSpecificity === null) { + $a = 0; + /// @todo should exclude \# as well as "#" + $aMatches = null; + $b = substr_count($this->sSelector, '#'); + $c = preg_match_all(self::NON_ID_ATTRIBUTES_AND_PSEUDO_CLASSES_RX, $this->sSelector, $aMatches); + $d = preg_match_all(self::ELEMENTS_AND_PSEUDO_ELEMENTS_RX, $this->sSelector, $aMatches); + $this->iSpecificity = ($a * 1000) + ($b * 100) + ($c * 10) + $d; + } + return $this->iSpecificity; + } } diff --git a/lib/php-css-parser/Renderable.php b/lib/php-css-parser/Renderable.php index 3ac06652e51..dc1bff3c1e9 100644 --- a/lib/php-css-parser/Renderable.php +++ b/lib/php-css-parser/Renderable.php @@ -2,8 +2,20 @@ namespace Sabberworm\CSS; -interface Renderable { - public function __toString(); - public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat); - public function getLineNo(); -} \ No newline at end of file +interface Renderable +{ + /** + * @return string + */ + public function __toString(); + + /** + * @return string + */ + public function render(OutputFormat $oOutputFormat); + + /** + * @return int + */ + public function getLineNo(); +} diff --git a/lib/php-css-parser/Rule/Rule.php b/lib/php-css-parser/Rule/Rule.php index 4480948f488..c1ea6df74e5 100644 --- a/lib/php-css-parser/Rule/Rule.php +++ b/lib/php-css-parser/Rule/Rule.php @@ -2,8 +2,12 @@ namespace Sabberworm\CSS\Rule; +use Sabberworm\CSS\Comment\Comment; use Sabberworm\CSS\Comment\Commentable; +use Sabberworm\CSS\OutputFormat; use Sabberworm\CSS\Parsing\ParserState; +use Sabberworm\CSS\Parsing\UnexpectedEOFException; +use Sabberworm\CSS\Parsing\UnexpectedTokenException; use Sabberworm\CSS\Renderable; use Sabberworm\CSS\Value\RuleValueList; use Sabberworm\CSS\Value\Value; @@ -12,224 +16,377 @@ use Sabberworm\CSS\Value\Value; * RuleSets contains Rule objects which always have a key and a value. * In CSS, Rules are expressed as follows: “key: value[0][0] value[0][1], value[1][0] value[1][1];” */ -class Rule implements Renderable, Commentable { +class Rule implements Renderable, Commentable +{ + /** + * @var string + */ + private $sRule; - private $sRule; - private $mValue; - private $bIsImportant; - private $aIeHack; - protected $iLineNo; - protected $aComments; + /** + * @var RuleValueList|null + */ + private $mValue; - public function __construct($sRule, $iLineNo = 0) { - $this->sRule = $sRule; - $this->mValue = null; - $this->bIsImportant = false; - $this->aIeHack = array(); - $this->iLineNo = $iLineNo; - $this->aComments = array(); - } + /** + * @var bool + */ + private $bIsImportant; - public static function parse(ParserState $oParserState) { - $aComments = $oParserState->consumeWhiteSpace(); - $oRule = new Rule($oParserState->parseIdentifier(), $oParserState->currentLine()); - $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(';'); - } + /** + * @var array + */ + private $aIeHack; - return $oRule; - } + /** + * @var int + */ + protected $iLineNo; - private static function listDelimiterForRule($sRule) { - if (preg_match('/^font($|-)/', $sRule)) { - return array(',', '/', ' '); - } - return array(',', ' ', '/'); - } + /** + * @var int + */ + protected $iColNo; - /** - * @return int - */ - public function getLineNo() { - return $this->iLineNo; - } + /** + * @var array + */ + protected $aComments; - public function setRule($sRule) { - $this->sRule = $sRule; - } + /** + * @param string $sRule + * @param int $iLineNo + * @param int $iColNo + */ + public function __construct($sRule, $iLineNo = 0, $iColNo = 0) + { + $this->sRule = $sRule; + $this->mValue = null; + $this->bIsImportant = false; + $this->aIeHack = []; + $this->iLineNo = $iLineNo; + $this->iColNo = $iColNo; + $this->aComments = []; + } - public function getRule() { - return $this->sRule; - } + /** + * @return Rule + * + * @throws UnexpectedEOFException + * @throws UnexpectedTokenException + */ + public static function parse(ParserState $oParserState) + { + $aComments = $oParserState->consumeWhiteSpace(); + $oRule = new Rule( + $oParserState->parseIdentifier(!$oParserState->comes("--")), + $oParserState->currentLine(), + $oParserState->currentColumn() + ); + $oRule->setComments($aComments); + $oRule->addComments($oParserState->consumeWhiteSpace()); + $oParserState->consume(':'); + $oValue = Value::parseValue($oParserState, self::listDelimiterForRule($oRule->getRule())); + $oRule->setValue($oValue); + if ($oParserState->getSettings()->bLenientParsing) { + while ($oParserState->comes('\\')) { + $oParserState->consume('\\'); + $oRule->addIeHack($oParserState->consume()); + $oParserState->consumeWhiteSpace(); + } + } + $oParserState->consumeWhiteSpace(); + if ($oParserState->comes('!')) { + $oParserState->consume('!'); + $oParserState->consumeWhiteSpace(); + $oParserState->consume('important'); + $oRule->setIsImportant(true); + } + $oParserState->consumeWhiteSpace(); + while ($oParserState->comes(';')) { + $oParserState->consume(';'); + } + $oParserState->consumeWhiteSpace(); - public function getValue() { - return $this->mValue; - } + return $oRule; + } - public function setValue($mValue) { - $this->mValue = $mValue; - } + /** + * @param string $sRule + * + * @return array + */ + private static function listDelimiterForRule($sRule) + { + if (preg_match('/^font($|-)/', $sRule)) { + return [',', '/', ' ']; + } + return [',', ' ', '/']; + } - /** - * @deprecated Old-Style 2-dimensional array given. Retained for (some) backwards-compatibility. Use setValue() instead and wrapp the value inside a RuleValueList if necessary. - */ - public function setValues($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 int + */ + public function getLineNo() + { + return $this->iLineNo; + } - /** - * @deprecated Old-Style 2-dimensional array returned. Retained for (some) backwards-compatibility. Use getValue() instead and check for the existance of a (nested set of) ValueList object(s). - */ - public function getValues() { - if (!$this->mValue instanceof RuleValueList) { - return array(array($this->mValue)); - } - if ($this->mValue->getListSeparator() === ',') { - return array($this->mValue->getListComponents()); - } - $aResult = array(); - foreach ($this->mValue->getListComponents() as $mValue) { - if (!$mValue instanceof RuleValueList || $mValue->getListSeparator() !== ',') { - $aResult[] = array($mValue); - continue; - } - if ($this->mValue->getListSeparator() === ' ' || count($aResult) === 0) { - $aResult[] = array(); - } - foreach ($mValue->getListComponents() as $mValue) { - $aResult[count($aResult) - 1][] = $mValue; - } - } - return $aResult; - } + /** + * @return int + */ + public function getColNo() + { + return $this->iColNo; + } - /** - * 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. - */ - public function addValue($mValue, $sType = ' ') { - if (!is_array($mValue)) { - $mValue = array($mValue); - } - if (!$this->mValue instanceof RuleValueList || $this->mValue->getListSeparator() !== $sType) { - $mCurrentValue = $this->mValue; - $this->mValue = new RuleValueList($sType, $this->iLineNo); - if ($mCurrentValue) { - $this->mValue->addListComponent($mCurrentValue); - } - } - foreach ($mValue as $mValueItem) { - $this->mValue->addListComponent($mValueItem); - } - } + /** + * @param int $iLine + * @param int $iColumn + * + * @return void + */ + public function setPosition($iLine, $iColumn) + { + $this->iColNo = $iColumn; + $this->iLineNo = $iLine; + } - public function addIeHack($iModifier) { - $this->aIeHack[] = $iModifier; - } + /** + * @param string $sRule + * + * @return void + */ + public function setRule($sRule) + { + $this->sRule = $sRule; + } - public function setIeHack(array $aModifiers) { - $this->aIeHack = $aModifiers; - } + /** + * @return string + */ + public function getRule() + { + return $this->sRule; + } - public function getIeHack() { - return $this->aIeHack; - } + /** + * @return RuleValueList|null + */ + public function getValue() + { + return $this->mValue; + } - public function setIsImportant($bIsImportant) { - $this->bIsImportant = $bIsImportant; - } + /** + * @param RuleValueList|null $mValue + * + * @return void + */ + public function setValue($mValue) + { + $this->mValue = $mValue; + } - public function getIsImportant() { - return $this->bIsImportant; - } + /** + * @param array> $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; + } - public function __toString() { - return $this->render(new \Sabberworm\CSS\OutputFormat()); - } + /** + * @return array> + * + * @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; + } - public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) { - $sResult = "{$this->sRule}:{$oOutputFormat->spaceAfterRuleName()}"; - if ($this->mValue instanceof Value) { //Can also be a ValueList - $sResult .= $this->mValue->render($oOutputFormat); - } else { - $sResult .= $this->mValue; - } - if (!empty($this->aIeHack)) { - $sResult .= ' \\' . implode('\\', $this->aIeHack); - } - if ($this->bIsImportant) { - $sResult .= ' !important'; - } - $sResult .= ';'; - return $sResult; - } + /** + * 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 $mValue + * @param string $sType + * + * @return void + */ + public function addValue($mValue, $sType = ' ') + { + if (!is_array($mValue)) { + $mValue = [$mValue]; + } + if (!$this->mValue instanceof RuleValueList || $this->mValue->getListSeparator() !== $sType) { + $mCurrentValue = $this->mValue; + $this->mValue = new RuleValueList($sType, $this->iLineNo); + if ($mCurrentValue) { + $this->mValue->addListComponent($mCurrentValue); + } + } + foreach ($mValue as $mValueItem) { + $this->mValue->addListComponent($mValueItem); + } + } - /** - * @param array $aComments Array of comments. - */ - public function addComments(array $aComments) { - $this->aComments = array_merge($this->aComments, $aComments); - } + /** + * @param int $iModifier + * + * @return void + */ + public function addIeHack($iModifier) + { + $this->aIeHack[] = $iModifier; + } - /** - * @return array - */ - public function getComments() { - return $this->aComments; - } + /** + * @param array $aModifiers + * + * @return void + */ + public function setIeHack(array $aModifiers) + { + $this->aIeHack = $aModifiers; + } - /** - * @param array $aComments Array containing Comment objects. - */ - public function setComments(array $aComments) { - $this->aComments = $aComments; - } + /** + * @return array + */ + public function getIeHack() + { + return $this->aIeHack; + } + /** + * @param bool $bIsImportant + * + * @return void + */ + public function setIsImportant($bIsImportant) + { + $this->bIsImportant = $bIsImportant; + } + + /** + * @return bool + */ + public function getIsImportant() + { + return $this->bIsImportant; + } + + /** + * @return string + */ + public function __toString() + { + return $this->render(new OutputFormat()); + } + + /** + * @return string + */ + public function render(OutputFormat $oOutputFormat) + { + $sResult = "{$this->sRule}:{$oOutputFormat->spaceAfterRuleName()}"; + if ($this->mValue instanceof Value) { //Can also be a ValueList + $sResult .= $this->mValue->render($oOutputFormat); + } else { + $sResult .= $this->mValue; + } + if (!empty($this->aIeHack)) { + $sResult .= ' \\' . implode('\\', $this->aIeHack); + } + if ($this->bIsImportant) { + $sResult .= ' !important'; + } + $sResult .= ';'; + return $sResult; + } + + /** + * @param array $aComments + * + * @return void + */ + public function addComments(array $aComments) + { + $this->aComments = array_merge($this->aComments, $aComments); + } + + /** + * @return array + */ + public function getComments() + { + return $this->aComments; + } + + /** + * @param array $aComments + * + * @return void + */ + public function setComments(array $aComments) + { + $this->aComments = $aComments; + } } diff --git a/lib/php-css-parser/RuleSet/AtRuleSet.php b/lib/php-css-parser/RuleSet/AtRuleSet.php index a1042a95aec..88bc5bd3139 100644 --- a/lib/php-css-parser/RuleSet/AtRuleSet.php +++ b/lib/php-css-parser/RuleSet/AtRuleSet.php @@ -2,43 +2,72 @@ namespace Sabberworm\CSS\RuleSet; +use Sabberworm\CSS\OutputFormat; use Sabberworm\CSS\Property\AtRule; /** - * A RuleSet constructed by an unknown @-rule. @font-face rules are rendered into AtRuleSet objects. + * A RuleSet constructed by an unknown at-rule. `@font-face` rules are rendered into AtRuleSet objects. */ -class AtRuleSet extends RuleSet implements AtRule { +class AtRuleSet extends RuleSet implements AtRule +{ + /** + * @var string + */ + private $sType; - private $sType; - private $sArgs; + /** + * @var string + */ + private $sArgs; - public function __construct($sType, $sArgs = '', $iLineNo = 0) { - parent::__construct($iLineNo); - $this->sType = $sType; - $this->sArgs = $sArgs; - } + /** + * @param string $sType + * @param string $sArgs + * @param int $iLineNo + */ + public function __construct($sType, $sArgs = '', $iLineNo = 0) + { + parent::__construct($iLineNo); + $this->sType = $sType; + $this->sArgs = $sArgs; + } - public function atRuleName() { - return $this->sType; - } + /** + * @return string + */ + public function atRuleName() + { + return $this->sType; + } - public function atRuleArgs() { - return $this->sArgs; - } + /** + * @return string + */ + public function atRuleArgs() + { + return $this->sArgs; + } - public function __toString() { - return $this->render(new \Sabberworm\CSS\OutputFormat()); - } + /** + * @return string + */ + public function __toString() + { + return $this->render(new OutputFormat()); + } - public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) { - $sArgs = $this->sArgs; - if($sArgs) { - $sArgs = ' ' . $sArgs; - } - $sResult = "@{$this->sType}$sArgs{$oOutputFormat->spaceBeforeOpeningBrace()}{"; - $sResult .= parent::render($oOutputFormat); - $sResult .= '}'; - return $sResult; - } - -} \ No newline at end of file + /** + * @return string + */ + public function render(OutputFormat $oOutputFormat) + { + $sArgs = $this->sArgs; + if ($sArgs) { + $sArgs = ' ' . $sArgs; + } + $sResult = "@{$this->sType}$sArgs{$oOutputFormat->spaceBeforeOpeningBrace()}{"; + $sResult .= parent::render($oOutputFormat); + $sResult .= '}'; + return $sResult; + } +} diff --git a/lib/php-css-parser/RuleSet/DeclarationBlock.php b/lib/php-css-parser/RuleSet/DeclarationBlock.php index 6614b1d1e13..c27cdd4c81b 100644 --- a/lib/php-css-parser/RuleSet/DeclarationBlock.php +++ b/lib/php-css-parser/RuleSet/DeclarationBlock.php @@ -2,627 +2,830 @@ namespace Sabberworm\CSS\RuleSet; -use Sabberworm\CSS\Parsing\ParserState; +use Sabberworm\CSS\CSSList\CSSList; +use Sabberworm\CSS\CSSList\KeyFrame; +use Sabberworm\CSS\OutputFormat; use Sabberworm\CSS\Parsing\OutputException; +use Sabberworm\CSS\Parsing\ParserState; +use Sabberworm\CSS\Parsing\UnexpectedEOFException; +use Sabberworm\CSS\Parsing\UnexpectedTokenException; +use Sabberworm\CSS\Property\KeyframeSelector; use Sabberworm\CSS\Property\Selector; use Sabberworm\CSS\Rule\Rule; -use Sabberworm\CSS\Value\RuleValueList; -use Sabberworm\CSS\Value\Value; -use Sabberworm\CSS\Value\Size; use Sabberworm\CSS\Value\Color; +use Sabberworm\CSS\Value\RuleValueList; +use Sabberworm\CSS\Value\Size; use Sabberworm\CSS\Value\URL; +use Sabberworm\CSS\Value\Value; /** - * Declaration blocks are the parts of a css file which denote the rules belonging to a selector. - * Declaration blocks usually appear directly inside a Document or another CSSList (mostly a MediaQuery). + * Declaration blocks are the parts of a CSS file which denote the rules belonging to a selector. + * + * Declaration blocks usually appear directly inside a `Document` or another `CSSList` (mostly a `MediaQuery`). */ -class DeclarationBlock extends RuleSet { +class DeclarationBlock extends RuleSet +{ + /** + * @var array + */ + private $aSelectors; - private $aSelectors; + /** + * @param int $iLineNo + */ + public function __construct($iLineNo = 0) + { + parent::__construct($iLineNo); + $this->aSelectors = []; + } - public function __construct($iLineNo = 0) { - parent::__construct($iLineNo); - $this->aSelectors = array(); - } + /** + * @param CSSList|null $oList + * + * @return DeclarationBlock|false + * + * @throws UnexpectedTokenException + * @throws UnexpectedEOFException + */ + public static function parse(ParserState $oParserState, $oList = null) + { + $aComments = []; + $oResult = new DeclarationBlock($oParserState->currentLine()); + try { + $aSelectorParts = []; + $sStringWrapperChar = false; + do { + $aSelectorParts[] = $oParserState->consume(1) + . $oParserState->consumeUntil(['{', '}', '\'', '"'], false, false, $aComments); + if (in_array($oParserState->peek(), ['\'', '"']) && substr(end($aSelectorParts), -1) != "\\") { + if ($sStringWrapperChar === false) { + $sStringWrapperChar = $oParserState->peek(); + } elseif ($sStringWrapperChar == $oParserState->peek()) { + $sStringWrapperChar = false; + } + } + } while (!in_array($oParserState->peek(), ['{', '}']) || $sStringWrapperChar !== false); + $oResult->setSelectors(implode('', $aSelectorParts), $oList); + if ($oParserState->comes('{')) { + $oParserState->consume(1); + } + } catch (UnexpectedTokenException $e) { + if ($oParserState->getSettings()->bLenientParsing) { + if (!$oParserState->comes('}')) { + $oParserState->consumeUntil('}', false, true); + } + return false; + } else { + throw $e; + } + } + $oResult->setComments($aComments); + RuleSet::parseRuleSet($oParserState, $oResult); + return $oResult; + } - public static function parse(ParserState $oParserState) { - $aComments = array(); - $oResult = new DeclarationBlock($oParserState->currentLine()); - $oResult->setSelector($oParserState->consumeUntil('{', false, true, $aComments)); - $oResult->setComments($aComments); - RuleSet::parseRuleSet($oParserState, $oResult); - return $oResult; - } + /** + * @param array|string $mSelector + * @param CSSList|null $oList + * + * @throws UnexpectedTokenException + */ + public function setSelectors($mSelector, $oList = null) + { + if (is_array($mSelector)) { + $this->aSelectors = $mSelector; + } else { + $this->aSelectors = explode(',', $mSelector); + } + foreach ($this->aSelectors as $iKey => $mSelector) { + if (!($mSelector instanceof Selector)) { + if ($oList === null || !($oList instanceof KeyFrame)) { + if (!Selector::isValid($mSelector)) { + throw new UnexpectedTokenException( + "Selector did not match '" . Selector::SELECTOR_VALIDATION_RX . "'.", + $mSelector, + "custom" + ); + } + $this->aSelectors[$iKey] = new Selector($mSelector); + } else { + if (!KeyframeSelector::isValid($mSelector)) { + throw new UnexpectedTokenException( + "Selector did not match '" . KeyframeSelector::SELECTOR_VALIDATION_RX . "'.", + $mSelector, + "custom" + ); + } + $this->aSelectors[$iKey] = new KeyframeSelector($mSelector); + } + } + } + } + /** + * Remove one of the selectors of the block. + * + * @param Selector|string $mSelector + * + * @return bool + */ + public function removeSelector($mSelector) + { + if ($mSelector instanceof Selector) { + $mSelector = $mSelector->getSelector(); + } + foreach ($this->aSelectors as $iKey => $oSelector) { + if ($oSelector->getSelector() === $mSelector) { + unset($this->aSelectors[$iKey]); + return true; + } + } + return false; + } - public function setSelectors($mSelector) { - if (is_array($mSelector)) { - $this->aSelectors = $mSelector; - } else { - $this->aSelectors = explode(',', $mSelector); - } - foreach ($this->aSelectors as $iKey => $mSelector) { - if (!($mSelector instanceof Selector)) { - $this->aSelectors[$iKey] = new Selector($mSelector); - } - } - } + /** + * @return array + * + * @deprecated will be removed in version 9.0; use `getSelectors()` instead + */ + public function getSelector() + { + return $this->getSelectors(); + } - // remove one of the selector of the block - public function removeSelector($mSelector) { - if($mSelector instanceof Selector) { - $mSelector = $mSelector->getSelector(); - } - foreach($this->aSelectors as $iKey => $oSelector) { - if($oSelector->getSelector() === $mSelector) { - unset($this->aSelectors[$iKey]); - return true; - } - } - return false; - } + /** + * @param Selector|string $mSelector + * @param CSSList|null $oList + * + * @return void + * + * @deprecated will be removed in version 9.0; use `setSelectors()` instead + */ + public function setSelector($mSelector, $oList = null) + { + $this->setSelectors($mSelector, $oList); + } - /** - * @deprecated use getSelectors() - */ - public function getSelector() { - return $this->getSelectors(); - } + /** + * @return array + */ + public function getSelectors() + { + return $this->aSelectors; + } - /** - * @deprecated use setSelectors() - */ - public function setSelector($mSelector) { - $this->setSelectors($mSelector); - } + /** + * Splits shorthand declarations (e.g. `margin` or `font`) into their constituent parts. + * + * @return void + */ + public function expandShorthands() + { + // border must be expanded before dimensions + $this->expandBorderShorthand(); + $this->expandDimensionsShorthand(); + $this->expandFontShorthand(); + $this->expandBackgroundShorthand(); + $this->expandListStyleShorthand(); + } - /** - * Get selectors. - * - * @return Selector[] Selectors. - */ - public function getSelectors() { - return $this->aSelectors; - } + /** + * Creates shorthand declarations (e.g. `margin` or `font`) whenever possible. + * + * @return void + */ + public function createShorthands() + { + $this->createBackgroundShorthand(); + $this->createDimensionsShorthand(); + // border must be shortened after dimensions + $this->createBorderShorthand(); + $this->createFontShorthand(); + $this->createListStyleShorthand(); + } - /** - * Split shorthand declarations (e.g. +margin+ or +font+) into their constituent parts. - * */ - public function expandShorthands() { - // border must be expanded before dimensions - $this->expandBorderShorthand(); - $this->expandDimensionsShorthand(); - $this->expandFontShorthand(); - $this->expandBackgroundShorthand(); - $this->expandListStyleShorthand(); - } + /** + * Splits shorthand border declarations (e.g. `border: 1px red;`). + * + * Additional splitting happens in expandDimensionsShorthand. + * + * Multiple borders are not yet supported as of 3. + * + * @return void + */ + public function expandBorderShorthand() + { + $aBorderRules = [ + 'border', + 'border-left', + 'border-right', + 'border-top', + 'border-bottom', + ]; + $aBorderSizes = [ + 'thin', + 'medium', + 'thick', + ]; + $aRules = $this->getRulesAssoc(); + foreach ($aBorderRules as $sBorderRule) { + if (!isset($aRules[$sBorderRule])) { + continue; + } + $oRule = $aRules[$sBorderRule]; + $mRuleValue = $oRule->getValue(); + $aValues = []; + if (!$mRuleValue instanceof RuleValueList) { + $aValues[] = $mRuleValue; + } else { + $aValues = $mRuleValue->getListComponents(); + } + foreach ($aValues as $mValue) { + if ($mValue instanceof Value) { + $mNewValue = clone $mValue; + } else { + $mNewValue = $mValue; + } + if ($mValue instanceof Size) { + $sNewRuleName = $sBorderRule . "-width"; + } elseif ($mValue instanceof Color) { + $sNewRuleName = $sBorderRule . "-color"; + } else { + if (in_array($mValue, $aBorderSizes)) { + $sNewRuleName = $sBorderRule . "-width"; + } else { + $sNewRuleName = $sBorderRule . "-style"; + } + } + $oNewRule = new Rule($sNewRuleName, $oRule->getLineNo(), $oRule->getColNo()); + $oNewRule->setIsImportant($oRule->getIsImportant()); + $oNewRule->addValue([$mNewValue]); + $this->addRule($oNewRule); + } + $this->removeRule($sBorderRule); + } + } - /** - * Create shorthand declarations (e.g. +margin+ or +font+) whenever possible. - * */ - public function createShorthands() { - $this->createBackgroundShorthand(); - $this->createDimensionsShorthand(); - // border must be shortened after dimensions - $this->createBorderShorthand(); - $this->createFontShorthand(); - $this->createListStyleShorthand(); - } + /** + * Splits shorthand dimensional declarations (e.g. `margin: 0px auto;`) + * into their constituent parts. + * + * Handles `margin`, `padding`, `border-color`, `border-style` and `border-width`. + * + * @return void + */ + public function expandDimensionsShorthand() + { + $aExpansions = [ + 'margin' => 'margin-%s', + 'padding' => 'padding-%s', + 'border-color' => 'border-%s-color', + 'border-style' => 'border-%s-style', + 'border-width' => 'border-%s-width', + ]; + $aRules = $this->getRulesAssoc(); + foreach ($aExpansions as $sProperty => $sExpanded) { + if (!isset($aRules[$sProperty])) { + continue; + } + $oRule = $aRules[$sProperty]; + $mRuleValue = $oRule->getValue(); + $aValues = []; + if (!$mRuleValue instanceof RuleValueList) { + $aValues[] = $mRuleValue; + } else { + $aValues = $mRuleValue->getListComponents(); + } + $top = $right = $bottom = $left = null; + switch (count($aValues)) { + case 1: + $top = $right = $bottom = $left = $aValues[0]; + break; + case 2: + $top = $bottom = $aValues[0]; + $left = $right = $aValues[1]; + break; + case 3: + $top = $aValues[0]; + $left = $right = $aValues[1]; + $bottom = $aValues[2]; + break; + case 4: + $top = $aValues[0]; + $right = $aValues[1]; + $bottom = $aValues[2]; + $left = $aValues[3]; + break; + } + foreach (['top', 'right', 'bottom', 'left'] as $sPosition) { + $oNewRule = new Rule(sprintf($sExpanded, $sPosition), $oRule->getLineNo(), $oRule->getColNo()); + $oNewRule->setIsImportant($oRule->getIsImportant()); + $oNewRule->addValue(${$sPosition}); + $this->addRule($oNewRule); + } + $this->removeRule($sProperty); + } + } - /** - * Split shorthand border declarations (e.g. border: 1px red;) - * Additional splitting happens in expandDimensionsShorthand - * Multiple borders are not yet supported as of 3 - * */ - public function expandBorderShorthand() { - $aBorderRules = array( - 'border', 'border-left', 'border-right', 'border-top', 'border-bottom' - ); - $aBorderSizes = array( - 'thin', 'medium', 'thick' - ); - $aRules = $this->getRulesAssoc(); - foreach ($aBorderRules as $sBorderRule) { - if (!isset($aRules[$sBorderRule])) - continue; - $oRule = $aRules[$sBorderRule]; - $mRuleValue = $oRule->getValue(); - $aValues = array(); - if (!$mRuleValue instanceof RuleValueList) { - $aValues[] = $mRuleValue; - } else { - $aValues = $mRuleValue->getListComponents(); - } - foreach ($aValues as $mValue) { - if ($mValue instanceof Value) { - $mNewValue = clone $mValue; - } else { - $mNewValue = $mValue; - } - if ($mValue instanceof Size) { - $sNewRuleName = $sBorderRule . "-width"; - } else if ($mValue instanceof Color) { - $sNewRuleName = $sBorderRule . "-color"; - } else { - if (in_array($mValue, $aBorderSizes)) { - $sNewRuleName = $sBorderRule . "-width"; - } else/* if(in_array($mValue, $aBorderStyles)) */ { - $sNewRuleName = $sBorderRule . "-style"; - } - } - $oNewRule = new Rule($sNewRuleName, $this->iLineNo); - $oNewRule->setIsImportant($oRule->getIsImportant()); - $oNewRule->addValue(array($mNewValue)); - $this->addRule($oNewRule); - } - $this->removeRule($sBorderRule); - } - } + /** + * Converts shorthand font declarations + * (e.g. `font: 300 italic 11px/14px verdana, helvetica, sans-serif;`) + * into their constituent parts. + * + * @return void + */ + public function expandFontShorthand() + { + $aRules = $this->getRulesAssoc(); + if (!isset($aRules['font'])) { + return; + } + $oRule = $aRules['font']; + // reset properties to 'normal' per http://www.w3.org/TR/21/fonts.html#font-shorthand + $aFontProperties = [ + 'font-style' => 'normal', + 'font-variant' => 'normal', + 'font-weight' => 'normal', + 'font-size' => 'normal', + 'line-height' => 'normal', + ]; + $mRuleValue = $oRule->getValue(); + $aValues = []; + if (!$mRuleValue instanceof RuleValueList) { + $aValues[] = $mRuleValue; + } else { + $aValues = $mRuleValue->getListComponents(); + } + foreach ($aValues as $mValue) { + if (!$mValue instanceof Value) { + $mValue = mb_strtolower($mValue); + } + if (in_array($mValue, ['normal', 'inherit'])) { + foreach (['font-style', 'font-weight', 'font-variant'] as $sProperty) { + if (!isset($aFontProperties[$sProperty])) { + $aFontProperties[$sProperty] = $mValue; + } + } + } elseif (in_array($mValue, ['italic', 'oblique'])) { + $aFontProperties['font-style'] = $mValue; + } elseif ($mValue == 'small-caps') { + $aFontProperties['font-variant'] = $mValue; + } elseif ( + in_array($mValue, ['bold', 'bolder', 'lighter']) + || ($mValue instanceof Size + && in_array($mValue->getSize(), range(100, 900, 100))) + ) { + $aFontProperties['font-weight'] = $mValue; + } elseif ($mValue instanceof RuleValueList && $mValue->getListSeparator() == '/') { + list($oSize, $oHeight) = $mValue->getListComponents(); + $aFontProperties['font-size'] = $oSize; + $aFontProperties['line-height'] = $oHeight; + } elseif ($mValue instanceof Size && $mValue->getUnit() !== null) { + $aFontProperties['font-size'] = $mValue; + } else { + $aFontProperties['font-family'] = $mValue; + } + } + foreach ($aFontProperties as $sProperty => $mValue) { + $oNewRule = new Rule($sProperty, $oRule->getLineNo(), $oRule->getColNo()); + $oNewRule->addValue($mValue); + $oNewRule->setIsImportant($oRule->getIsImportant()); + $this->addRule($oNewRule); + } + $this->removeRule('font'); + } - /** - * Split shorthand dimensional declarations (e.g. margin: 0px auto;) - * into their constituent parts. - * Handles margin, padding, border-color, border-style and border-width. - * */ - public function expandDimensionsShorthand() { - $aExpansions = array( - 'margin' => 'margin-%s', - 'padding' => 'padding-%s', - 'border-color' => 'border-%s-color', - 'border-style' => 'border-%s-style', - 'border-width' => 'border-%s-width' - ); - $aRules = $this->getRulesAssoc(); - foreach ($aExpansions as $sProperty => $sExpanded) { - if (!isset($aRules[$sProperty])) - continue; - $oRule = $aRules[$sProperty]; - $mRuleValue = $oRule->getValue(); - $aValues = array(); - if (!$mRuleValue instanceof RuleValueList) { - $aValues[] = $mRuleValue; - } else { - $aValues = $mRuleValue->getListComponents(); - } - $top = $right = $bottom = $left = null; - switch (count($aValues)) { - case 1: - $top = $right = $bottom = $left = $aValues[0]; - break; - case 2: - $top = $bottom = $aValues[0]; - $left = $right = $aValues[1]; - break; - case 3: - $top = $aValues[0]; - $left = $right = $aValues[1]; - $bottom = $aValues[2]; - break; - case 4: - $top = $aValues[0]; - $right = $aValues[1]; - $bottom = $aValues[2]; - $left = $aValues[3]; - break; - } - foreach (array('top', 'right', 'bottom', 'left') as $sPosition) { - $oNewRule = new Rule(sprintf($sExpanded, $sPosition), $this->iLineNo); - $oNewRule->setIsImportant($oRule->getIsImportant()); - $oNewRule->addValue(${$sPosition}); - $this->addRule($oNewRule); - } - $this->removeRule($sProperty); - } - } + /** + * Converts shorthand background declarations + * (e.g. `background: url("chess.png") gray 50% repeat fixed;`) + * into their constituent parts. + * + * @see http://www.w3.org/TR/21/colors.html#propdef-background + * + * @return void + */ + public function expandBackgroundShorthand() + { + $aRules = $this->getRulesAssoc(); + if (!isset($aRules['background'])) { + return; + } + $oRule = $aRules['background']; + $aBgProperties = [ + 'background-color' => ['transparent'], + 'background-image' => ['none'], + 'background-repeat' => ['repeat'], + 'background-attachment' => ['scroll'], + 'background-position' => [ + new Size(0, '%', null, false, $this->iLineNo), + new Size(0, '%', null, false, $this->iLineNo), + ], + ]; + $mRuleValue = $oRule->getValue(); + $aValues = []; + if (!$mRuleValue instanceof RuleValueList) { + $aValues[] = $mRuleValue; + } else { + $aValues = $mRuleValue->getListComponents(); + } + if (count($aValues) == 1 && $aValues[0] == 'inherit') { + foreach ($aBgProperties as $sProperty => $mValue) { + $oNewRule = new Rule($sProperty, $oRule->getLineNo(), $oRule->getColNo()); + $oNewRule->addValue('inherit'); + $oNewRule->setIsImportant($oRule->getIsImportant()); + $this->addRule($oNewRule); + } + $this->removeRule('background'); + return; + } + $iNumBgPos = 0; + foreach ($aValues as $mValue) { + if (!$mValue instanceof Value) { + $mValue = mb_strtolower($mValue); + } + if ($mValue instanceof URL) { + $aBgProperties['background-image'] = $mValue; + } elseif ($mValue instanceof Color) { + $aBgProperties['background-color'] = $mValue; + } elseif (in_array($mValue, ['scroll', 'fixed'])) { + $aBgProperties['background-attachment'] = $mValue; + } elseif (in_array($mValue, ['repeat', 'no-repeat', 'repeat-x', 'repeat-y'])) { + $aBgProperties['background-repeat'] = $mValue; + } elseif ( + in_array($mValue, ['left', 'center', 'right', 'top', 'bottom']) + || $mValue instanceof Size + ) { + if ($iNumBgPos == 0) { + $aBgProperties['background-position'][0] = $mValue; + $aBgProperties['background-position'][1] = 'center'; + } else { + $aBgProperties['background-position'][$iNumBgPos] = $mValue; + } + $iNumBgPos++; + } + } + foreach ($aBgProperties as $sProperty => $mValue) { + $oNewRule = new Rule($sProperty, $oRule->getLineNo(), $oRule->getColNo()); + $oNewRule->setIsImportant($oRule->getIsImportant()); + $oNewRule->addValue($mValue); + $this->addRule($oNewRule); + } + $this->removeRule('background'); + } - /** - * Convert shorthand font declarations - * (e.g. font: 300 italic 11px/14px verdana, helvetica, sans-serif;) - * into their constituent parts. - * */ - public function expandFontShorthand() { - $aRules = $this->getRulesAssoc(); - if (!isset($aRules['font'])) - return; - $oRule = $aRules['font']; - // reset properties to 'normal' per http://www.w3.org/TR/21/fonts.html#font-shorthand - $aFontProperties = array( - 'font-style' => 'normal', - 'font-variant' => 'normal', - 'font-weight' => 'normal', - 'font-size' => 'normal', - 'line-height' => 'normal' - ); - $mRuleValue = $oRule->getValue(); - $aValues = array(); - if (!$mRuleValue instanceof RuleValueList) { - $aValues[] = $mRuleValue; - } else { - $aValues = $mRuleValue->getListComponents(); - } - foreach ($aValues as $mValue) { - if (!$mValue instanceof Value) { - $mValue = mb_strtolower($mValue); - } - if (in_array($mValue, array('normal', 'inherit'))) { - foreach (array('font-style', 'font-weight', 'font-variant') as $sProperty) { - if (!isset($aFontProperties[$sProperty])) { - $aFontProperties[$sProperty] = $mValue; - } - } - } else if (in_array($mValue, array('italic', 'oblique'))) { - $aFontProperties['font-style'] = $mValue; - } else if ($mValue == 'small-caps') { - $aFontProperties['font-variant'] = $mValue; - } else if ( - in_array($mValue, array('bold', 'bolder', 'lighter')) - || ($mValue instanceof Size - && in_array($mValue->getSize(), range(100, 900, 100))) - ) { - $aFontProperties['font-weight'] = $mValue; - } else if ($mValue instanceof RuleValueList && $mValue->getListSeparator() == '/') { - list($oSize, $oHeight) = $mValue->getListComponents(); - $aFontProperties['font-size'] = $oSize; - $aFontProperties['line-height'] = $oHeight; - } else if ($mValue instanceof Size && $mValue->getUnit() !== null) { - $aFontProperties['font-size'] = $mValue; - } else { - $aFontProperties['font-family'] = $mValue; - } - } - foreach ($aFontProperties as $sProperty => $mValue) { - $oNewRule = new Rule($sProperty, $this->iLineNo); - $oNewRule->addValue($mValue); - $oNewRule->setIsImportant($oRule->getIsImportant()); - $this->addRule($oNewRule); - } - $this->removeRule('font'); - } + /** + * @return void + */ + public function expandListStyleShorthand() + { + $aListProperties = [ + 'list-style-type' => 'disc', + 'list-style-position' => 'outside', + 'list-style-image' => 'none', + ]; + $aListStyleTypes = [ + 'none', + 'disc', + 'circle', + 'square', + 'decimal-leading-zero', + 'decimal', + 'lower-roman', + 'upper-roman', + 'lower-greek', + 'lower-alpha', + 'lower-latin', + 'upper-alpha', + 'upper-latin', + 'hebrew', + 'armenian', + 'georgian', + 'cjk-ideographic', + 'hiragana', + 'hira-gana-iroha', + 'katakana-iroha', + 'katakana', + ]; + $aListStylePositions = [ + 'inside', + 'outside', + ]; + $aRules = $this->getRulesAssoc(); + if (!isset($aRules['list-style'])) { + return; + } + $oRule = $aRules['list-style']; + $mRuleValue = $oRule->getValue(); + $aValues = []; + if (!$mRuleValue instanceof RuleValueList) { + $aValues[] = $mRuleValue; + } else { + $aValues = $mRuleValue->getListComponents(); + } + if (count($aValues) == 1 && $aValues[0] == 'inherit') { + foreach ($aListProperties as $sProperty => $mValue) { + $oNewRule = new Rule($sProperty, $oRule->getLineNo(), $oRule->getColNo()); + $oNewRule->addValue('inherit'); + $oNewRule->setIsImportant($oRule->getIsImportant()); + $this->addRule($oNewRule); + } + $this->removeRule('list-style'); + return; + } + foreach ($aValues as $mValue) { + if (!$mValue instanceof Value) { + $mValue = mb_strtolower($mValue); + } + if ($mValue instanceof Url) { + $aListProperties['list-style-image'] = $mValue; + } elseif (in_array($mValue, $aListStyleTypes)) { + $aListProperties['list-style-types'] = $mValue; + } elseif (in_array($mValue, $aListStylePositions)) { + $aListProperties['list-style-position'] = $mValue; + } + } + foreach ($aListProperties as $sProperty => $mValue) { + $oNewRule = new Rule($sProperty, $oRule->getLineNo(), $oRule->getColNo()); + $oNewRule->setIsImportant($oRule->getIsImportant()); + $oNewRule->addValue($mValue); + $this->addRule($oNewRule); + } + $this->removeRule('list-style'); + } - /* - * Convert shorthand background declarations - * (e.g. background: url("chess.png") gray 50% repeat fixed;) - * into their constituent parts. - * @see http://www.w3.org/TR/21/colors.html#propdef-background - * */ + /** + * @param array $aProperties + * @param string $sShorthand + * + * @return void + */ + public function createShorthandProperties(array $aProperties, $sShorthand) + { + $aRules = $this->getRulesAssoc(); + $aNewValues = []; + foreach ($aProperties as $sProperty) { + if (!isset($aRules[$sProperty])) { + continue; + } + $oRule = $aRules[$sProperty]; + if (!$oRule->getIsImportant()) { + $mRuleValue = $oRule->getValue(); + $aValues = []; + if (!$mRuleValue instanceof RuleValueList) { + $aValues[] = $mRuleValue; + } else { + $aValues = $mRuleValue->getListComponents(); + } + foreach ($aValues as $mValue) { + $aNewValues[] = $mValue; + } + $this->removeRule($sProperty); + } + } + if (count($aNewValues)) { + $oNewRule = new Rule($sShorthand, $oRule->getLineNo(), $oRule->getColNo()); + foreach ($aNewValues as $mValue) { + $oNewRule->addValue($mValue); + } + $this->addRule($oNewRule); + } + } - public function expandBackgroundShorthand() { - $aRules = $this->getRulesAssoc(); - if (!isset($aRules['background'])) - return; - $oRule = $aRules['background']; - $aBgProperties = array( - 'background-color' => array('transparent'), 'background-image' => array('none'), - 'background-repeat' => array('repeat'), 'background-attachment' => array('scroll'), - 'background-position' => array(new Size(0, '%', null, false, $this->iLineNo), new Size(0, '%', null, false, $this->iLineNo)) - ); - $mRuleValue = $oRule->getValue(); - $aValues = array(); - if (!$mRuleValue instanceof RuleValueList) { - $aValues[] = $mRuleValue; - } else { - $aValues = $mRuleValue->getListComponents(); - } - if (count($aValues) == 1 && $aValues[0] == 'inherit') { - foreach ($aBgProperties as $sProperty => $mValue) { - $oNewRule = new Rule($sProperty, $this->iLineNo); - $oNewRule->addValue('inherit'); - $oNewRule->setIsImportant($oRule->getIsImportant()); - $this->addRule($oNewRule); - } - $this->removeRule('background'); - return; - } - $iNumBgPos = 0; - foreach ($aValues as $mValue) { - if (!$mValue instanceof Value) { - $mValue = mb_strtolower($mValue); - } - if ($mValue instanceof URL) { - $aBgProperties['background-image'] = $mValue; - } else if ($mValue instanceof Color) { - $aBgProperties['background-color'] = $mValue; - } else if (in_array($mValue, array('scroll', 'fixed'))) { - $aBgProperties['background-attachment'] = $mValue; - } else if (in_array($mValue, array('repeat', 'no-repeat', 'repeat-x', 'repeat-y'))) { - $aBgProperties['background-repeat'] = $mValue; - } else if (in_array($mValue, array('left', 'center', 'right', 'top', 'bottom')) - || $mValue instanceof Size - ) { - if ($iNumBgPos == 0) { - $aBgProperties['background-position'][0] = $mValue; - $aBgProperties['background-position'][1] = 'center'; - } else { - $aBgProperties['background-position'][$iNumBgPos] = $mValue; - } - $iNumBgPos++; - } - } - foreach ($aBgProperties as $sProperty => $mValue) { - $oNewRule = new Rule($sProperty, $this->iLineNo); - $oNewRule->setIsImportant($oRule->getIsImportant()); - $oNewRule->addValue($mValue); - $this->addRule($oNewRule); - } - $this->removeRule('background'); - } + /** + * @return void + */ + public function createBackgroundShorthand() + { + $aProperties = [ + 'background-color', + 'background-image', + 'background-repeat', + 'background-position', + 'background-attachment', + ]; + $this->createShorthandProperties($aProperties, 'background'); + } - public function expandListStyleShorthand() { - $aListProperties = array( - 'list-style-type' => 'disc', - 'list-style-position' => 'outside', - 'list-style-image' => 'none' - ); - $aListStyleTypes = array( - 'none', 'disc', 'circle', 'square', 'decimal-leading-zero', 'decimal', - 'lower-roman', 'upper-roman', 'lower-greek', 'lower-alpha', 'lower-latin', - 'upper-alpha', 'upper-latin', 'hebrew', 'armenian', 'georgian', 'cjk-ideographic', - 'hiragana', 'hira-gana-iroha', 'katakana-iroha', 'katakana' - ); - $aListStylePositions = array( - 'inside', 'outside' - ); - $aRules = $this->getRulesAssoc(); - if (!isset($aRules['list-style'])) - return; - $oRule = $aRules['list-style']; - $mRuleValue = $oRule->getValue(); - $aValues = array(); - if (!$mRuleValue instanceof RuleValueList) { - $aValues[] = $mRuleValue; - } else { - $aValues = $mRuleValue->getListComponents(); - } - if (count($aValues) == 1 && $aValues[0] == 'inherit') { - foreach ($aListProperties as $sProperty => $mValue) { - $oNewRule = new Rule($sProperty, $this->iLineNo); - $oNewRule->addValue('inherit'); - $oNewRule->setIsImportant($oRule->getIsImportant()); - $this->addRule($oNewRule); - } - $this->removeRule('list-style'); - return; - } - foreach ($aValues as $mValue) { - if (!$mValue instanceof Value) { - $mValue = mb_strtolower($mValue); - } - if ($mValue instanceof Url) { - $aListProperties['list-style-image'] = $mValue; - } else if (in_array($mValue, $aListStyleTypes)) { - $aListProperties['list-style-types'] = $mValue; - } else if (in_array($mValue, $aListStylePositions)) { - $aListProperties['list-style-position'] = $mValue; - } - } - foreach ($aListProperties as $sProperty => $mValue) { - $oNewRule = new Rule($sProperty, $this->iLineNo); - $oNewRule->setIsImportant($oRule->getIsImportant()); - $oNewRule->addValue($mValue); - $this->addRule($oNewRule); - } - $this->removeRule('list-style'); - } + /** + * @return void + */ + public function createListStyleShorthand() + { + $aProperties = [ + 'list-style-type', + 'list-style-position', + 'list-style-image', + ]; + $this->createShorthandProperties($aProperties, 'list-style'); + } - public function createShorthandProperties(array $aProperties, $sShorthand) { - $aRules = $this->getRulesAssoc(); - $aNewValues = array(); - foreach ($aProperties as $sProperty) { - if (!isset($aRules[$sProperty])) - continue; - $oRule = $aRules[$sProperty]; - if (!$oRule->getIsImportant()) { - $mRuleValue = $oRule->getValue(); - $aValues = array(); - if (!$mRuleValue instanceof RuleValueList) { - $aValues[] = $mRuleValue; - } else { - $aValues = $mRuleValue->getListComponents(); - } - foreach ($aValues as $mValue) { - $aNewValues[] = $mValue; - } - $this->removeRule($sProperty); - } - } - if (count($aNewValues)) { - $oNewRule = new Rule($sShorthand, $this->iLineNo); - foreach ($aNewValues as $mValue) { - $oNewRule->addValue($mValue); - } - $this->addRule($oNewRule); - } - } + /** + * Combines `border-color`, `border-style` and `border-width` into `border`. + * + * Should be run after `create_dimensions_shorthand`! + * + * @return void + */ + public function createBorderShorthand() + { + $aProperties = [ + 'border-width', + 'border-style', + 'border-color', + ]; + $this->createShorthandProperties($aProperties, 'border'); + } - public function createBackgroundShorthand() { - $aProperties = array( - 'background-color', 'background-image', 'background-repeat', - 'background-position', 'background-attachment' - ); - $this->createShorthandProperties($aProperties, 'background'); - } + /** + * Looks for long format CSS dimensional properties + * (margin, padding, border-color, border-style and border-width) + * and converts them into shorthand CSS properties. + * + * @return void + */ + public function createDimensionsShorthand() + { + $aPositions = ['top', 'right', 'bottom', 'left']; + $aExpansions = [ + 'margin' => 'margin-%s', + 'padding' => 'padding-%s', + 'border-color' => 'border-%s-color', + 'border-style' => 'border-%s-style', + 'border-width' => 'border-%s-width', + ]; + $aRules = $this->getRulesAssoc(); + foreach ($aExpansions as $sProperty => $sExpanded) { + $aFoldable = []; + foreach ($aRules as $sRuleName => $oRule) { + foreach ($aPositions as $sPosition) { + if ($sRuleName == sprintf($sExpanded, $sPosition)) { + $aFoldable[$sRuleName] = $oRule; + } + } + } + // All four dimensions must be present + if (count($aFoldable) == 4) { + $aValues = []; + foreach ($aPositions as $sPosition) { + $oRule = $aRules[sprintf($sExpanded, $sPosition)]; + $mRuleValue = $oRule->getValue(); + $aRuleValues = []; + if (!$mRuleValue instanceof RuleValueList) { + $aRuleValues[] = $mRuleValue; + } else { + $aRuleValues = $mRuleValue->getListComponents(); + } + $aValues[$sPosition] = $aRuleValues; + } + $oNewRule = new Rule($sProperty, $oRule->getLineNo(), $oRule->getColNo()); + if ((string)$aValues['left'][0] == (string)$aValues['right'][0]) { + if ((string)$aValues['top'][0] == (string)$aValues['bottom'][0]) { + if ((string)$aValues['top'][0] == (string)$aValues['left'][0]) { + // All 4 sides are equal + $oNewRule->addValue($aValues['top']); + } else { + // Top and bottom are equal, left and right are equal + $oNewRule->addValue($aValues['top']); + $oNewRule->addValue($aValues['left']); + } + } else { + // Only left and right are equal + $oNewRule->addValue($aValues['top']); + $oNewRule->addValue($aValues['left']); + $oNewRule->addValue($aValues['bottom']); + } + } else { + // No sides are equal + $oNewRule->addValue($aValues['top']); + $oNewRule->addValue($aValues['left']); + $oNewRule->addValue($aValues['bottom']); + $oNewRule->addValue($aValues['right']); + } + $this->addRule($oNewRule); + foreach ($aPositions as $sPosition) { + $this->removeRule(sprintf($sExpanded, $sPosition)); + } + } + } + } - public function createListStyleShorthand() { - $aProperties = array( - 'list-style-type', 'list-style-position', 'list-style-image' - ); - $this->createShorthandProperties($aProperties, 'list-style'); - } + /** + * Looks for long format CSS font properties (e.g. `font-weight`) and + * tries to convert them into a shorthand CSS `font` property. + * + * At least `font-size` AND `font-family` must be present in order to create a shorthand declaration. + * + * @return void + */ + public function createFontShorthand() + { + $aFontProperties = [ + 'font-style', + 'font-variant', + 'font-weight', + 'font-size', + 'line-height', + 'font-family', + ]; + $aRules = $this->getRulesAssoc(); + if (!isset($aRules['font-size']) || !isset($aRules['font-family'])) { + return; + } + $oOldRule = isset($aRules['font-size']) ? $aRules['font-size'] : $aRules['font-family']; + $oNewRule = new Rule('font', $oOldRule->getLineNo(), $oOldRule->getColNo()); + unset($oOldRule); + foreach (['font-style', 'font-variant', 'font-weight'] as $sProperty) { + if (isset($aRules[$sProperty])) { + $oRule = $aRules[$sProperty]; + $mRuleValue = $oRule->getValue(); + $aValues = []; + if (!$mRuleValue instanceof RuleValueList) { + $aValues[] = $mRuleValue; + } else { + $aValues = $mRuleValue->getListComponents(); + } + if ($aValues[0] !== 'normal') { + $oNewRule->addValue($aValues[0]); + } + } + } + // Get the font-size value + $oRule = $aRules['font-size']; + $mRuleValue = $oRule->getValue(); + $aFSValues = []; + if (!$mRuleValue instanceof RuleValueList) { + $aFSValues[] = $mRuleValue; + } else { + $aFSValues = $mRuleValue->getListComponents(); + } + // But wait to know if we have line-height to add it + if (isset($aRules['line-height'])) { + $oRule = $aRules['line-height']; + $mRuleValue = $oRule->getValue(); + $aLHValues = []; + if (!$mRuleValue instanceof RuleValueList) { + $aLHValues[] = $mRuleValue; + } else { + $aLHValues = $mRuleValue->getListComponents(); + } + if ($aLHValues[0] !== 'normal') { + $val = new RuleValueList('/', $this->iLineNo); + $val->addListComponent($aFSValues[0]); + $val->addListComponent($aLHValues[0]); + $oNewRule->addValue($val); + } + } else { + $oNewRule->addValue($aFSValues[0]); + } + $oRule = $aRules['font-family']; + $mRuleValue = $oRule->getValue(); + $aFFValues = []; + if (!$mRuleValue instanceof RuleValueList) { + $aFFValues[] = $mRuleValue; + } else { + $aFFValues = $mRuleValue->getListComponents(); + } + $oFFValue = new RuleValueList(',', $this->iLineNo); + $oFFValue->setListComponents($aFFValues); + $oNewRule->addValue($oFFValue); - /** - * Combine border-color, border-style and border-width into border - * Should be run after create_dimensions_shorthand! - * */ - public function createBorderShorthand() { - $aProperties = array( - 'border-width', 'border-style', 'border-color' - ); - $this->createShorthandProperties($aProperties, 'border'); - } + $this->addRule($oNewRule); + foreach ($aFontProperties as $sProperty) { + $this->removeRule($sProperty); + } + } - /* - * Looks for long format CSS dimensional properties - * (margin, padding, border-color, border-style and border-width) - * and converts them into shorthand CSS properties. - * */ - - public function createDimensionsShorthand() { - $aPositions = array('top', 'right', 'bottom', 'left'); - $aExpansions = array( - 'margin' => 'margin-%s', - 'padding' => 'padding-%s', - 'border-color' => 'border-%s-color', - 'border-style' => 'border-%s-style', - 'border-width' => 'border-%s-width' - ); - $aRules = $this->getRulesAssoc(); - foreach ($aExpansions as $sProperty => $sExpanded) { - $aFoldable = array(); - foreach ($aRules as $sRuleName => $oRule) { - foreach ($aPositions as $sPosition) { - if ($sRuleName == sprintf($sExpanded, $sPosition)) { - $aFoldable[$sRuleName] = $oRule; - } - } - } - // All four dimensions must be present - if (count($aFoldable) == 4) { - $aValues = array(); - foreach ($aPositions as $sPosition) { - $oRule = $aRules[sprintf($sExpanded, $sPosition)]; - $mRuleValue = $oRule->getValue(); - $aRuleValues = array(); - if (!$mRuleValue instanceof RuleValueList) { - $aRuleValues[] = $mRuleValue; - } else { - $aRuleValues = $mRuleValue->getListComponents(); - } - $aValues[$sPosition] = $aRuleValues; - } - $oNewRule = new Rule($sProperty, $this->iLineNo); - if ((string) $aValues['left'][0] == (string) $aValues['right'][0]) { - if ((string) $aValues['top'][0] == (string) $aValues['bottom'][0]) { - if ((string) $aValues['top'][0] == (string) $aValues['left'][0]) { - // All 4 sides are equal - $oNewRule->addValue($aValues['top']); - } else { - // Top and bottom are equal, left and right are equal - $oNewRule->addValue($aValues['top']); - $oNewRule->addValue($aValues['left']); - } - } else { - // Only left and right are equal - $oNewRule->addValue($aValues['top']); - $oNewRule->addValue($aValues['left']); - $oNewRule->addValue($aValues['bottom']); - } - } else { - // No sides are equal - $oNewRule->addValue($aValues['top']); - $oNewRule->addValue($aValues['left']); - $oNewRule->addValue($aValues['bottom']); - $oNewRule->addValue($aValues['right']); - } - $this->addRule($oNewRule); - foreach ($aPositions as $sPosition) { - $this->removeRule(sprintf($sExpanded, $sPosition)); - } - } - } - } - - /** - * Looks for long format CSS font properties (e.g. font-weight) and - * tries to convert them into a shorthand CSS font property. - * At least font-size AND font-family must be present in order to create a shorthand declaration. - * */ - public function createFontShorthand() { - $aFontProperties = array( - 'font-style', 'font-variant', 'font-weight', 'font-size', 'line-height', 'font-family' - ); - $aRules = $this->getRulesAssoc(); - if (!isset($aRules['font-size']) || !isset($aRules['font-family'])) { - return; - } - $oNewRule = new Rule('font', $this->iLineNo); - foreach (array('font-style', 'font-variant', 'font-weight') as $sProperty) { - if (isset($aRules[$sProperty])) { - $oRule = $aRules[$sProperty]; - $mRuleValue = $oRule->getValue(); - $aValues = array(); - if (!$mRuleValue instanceof RuleValueList) { - $aValues[] = $mRuleValue; - } else { - $aValues = $mRuleValue->getListComponents(); - } - if ($aValues[0] !== 'normal') { - $oNewRule->addValue($aValues[0]); - } - } - } - // Get the font-size value - $oRule = $aRules['font-size']; - $mRuleValue = $oRule->getValue(); - $aFSValues = array(); - if (!$mRuleValue instanceof RuleValueList) { - $aFSValues[] = $mRuleValue; - } else { - $aFSValues = $mRuleValue->getListComponents(); - } - // But wait to know if we have line-height to add it - if (isset($aRules['line-height'])) { - $oRule = $aRules['line-height']; - $mRuleValue = $oRule->getValue(); - $aLHValues = array(); - if (!$mRuleValue instanceof RuleValueList) { - $aLHValues[] = $mRuleValue; - } else { - $aLHValues = $mRuleValue->getListComponents(); - } - if ($aLHValues[0] !== 'normal') { - $val = new RuleValueList('/', $this->iLineNo); - $val->addListComponent($aFSValues[0]); - $val->addListComponent($aLHValues[0]); - $oNewRule->addValue($val); - } - } else { - $oNewRule->addValue($aFSValues[0]); - } - $oRule = $aRules['font-family']; - $mRuleValue = $oRule->getValue(); - $aFFValues = array(); - if (!$mRuleValue instanceof RuleValueList) { - $aFFValues[] = $mRuleValue; - } else { - $aFFValues = $mRuleValue->getListComponents(); - } - $oFFValue = new RuleValueList(',', $this->iLineNo); - $oFFValue->setListComponents($aFFValues); - $oNewRule->addValue($oFFValue); - - $this->addRule($oNewRule); - foreach ($aFontProperties as $sProperty) { - $this->removeRule($sProperty); - } - } - - public function __toString() { - return $this->render(new \Sabberworm\CSS\OutputFormat()); - } - - public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) { - if(count($this->aSelectors) === 0) { - // If all the selectors have been removed, this declaration block becomes invalid - throw new OutputException("Attempt to print declaration block with missing selector", $this->iLineNo); - } - $sResult = $oOutputFormat->sBeforeDeclarationBlock; - $sResult .= $oOutputFormat->implode($oOutputFormat->spaceBeforeSelectorSeparator() . ',' . $oOutputFormat->spaceAfterSelectorSeparator(), $this->aSelectors); - $sResult .= $oOutputFormat->sAfterDeclarationBlockSelectors; - $sResult .= $oOutputFormat->spaceBeforeOpeningBrace() . '{'; - $sResult .= parent::render($oOutputFormat); - $sResult .= '}'; - $sResult .= $oOutputFormat->sAfterDeclarationBlock; - return $sResult; - } + /** + * @return string + * + * @throws OutputException + */ + public function __toString() + { + return $this->render(new OutputFormat()); + } + /** + * @return string + * + * @throws OutputException + */ + public function render(OutputFormat $oOutputFormat) + { + if (count($this->aSelectors) === 0) { + // If all the selectors have been removed, this declaration block becomes invalid + throw new OutputException("Attempt to print declaration block with missing selector", $this->iLineNo); + } + $sResult = $oOutputFormat->sBeforeDeclarationBlock; + $sResult .= $oOutputFormat->implode( + $oOutputFormat->spaceBeforeSelectorSeparator() . ',' . $oOutputFormat->spaceAfterSelectorSeparator(), + $this->aSelectors + ); + $sResult .= $oOutputFormat->sAfterDeclarationBlockSelectors; + $sResult .= $oOutputFormat->spaceBeforeOpeningBrace() . '{'; + $sResult .= parent::render($oOutputFormat); + $sResult .= '}'; + $sResult .= $oOutputFormat->sAfterDeclarationBlock; + return $sResult; + } } diff --git a/lib/php-css-parser/RuleSet/RuleSet.php b/lib/php-css-parser/RuleSet/RuleSet.php index e5d5e41540f..9404bb0bdb9 100644 --- a/lib/php-css-parser/RuleSet/RuleSet.php +++ b/lib/php-css-parser/RuleSet/RuleSet.php @@ -2,211 +2,325 @@ namespace Sabberworm\CSS\RuleSet; +use Sabberworm\CSS\Comment\Comment; use Sabberworm\CSS\Comment\Commentable; +use Sabberworm\CSS\OutputFormat; use Sabberworm\CSS\Parsing\ParserState; +use Sabberworm\CSS\Parsing\UnexpectedEOFException; use Sabberworm\CSS\Parsing\UnexpectedTokenException; use Sabberworm\CSS\Renderable; use Sabberworm\CSS\Rule\Rule; /** * RuleSet is a generic superclass denoting rules. The typical example for rule sets are declaration block. - * However, unknown At-Rules (like @font-face) are also rule sets. + * However, unknown At-Rules (like `@font-face`) are also rule sets. */ -abstract class RuleSet implements Renderable, Commentable { +abstract class RuleSet implements Renderable, Commentable +{ + /** + * @var array + */ + private $aRules; - private $aRules; - protected $iLineNo; - protected $aComments; + /** + * @var int + */ + protected $iLineNo; - public function __construct($iLineNo = 0) { - $this->aRules = array(); - $this->iLineNo = $iLineNo; - $this->aComments = array(); - } + /** + * @var array + */ + protected $aComments; - public static function parseRuleSet(ParserState $oParserState, RuleSet $oRuleSet) { - while ($oParserState->comes(';')) { - $oParserState->consume(';'); - } - while (!$oParserState->comes('}')) { - $oRule = null; - if($oParserState->getSettings()->bLenientParsing) { - try { - $oRule = Rule::parse($oParserState); - } catch (UnexpectedTokenException $e) { - try { - $sConsume = $oParserState->consumeUntil(array("\n", ";", '}'), true); - // We need to “unfind” the matches to the end of the ruleSet as this will be matched later - if($oParserState->streql(substr($sConsume, -1), '}')) { - $oParserState->backtrack(1); - } else { - while ($oParserState->comes(';')) { - $oParserState->consume(';'); - } - } - } catch (UnexpectedTokenException $e) { - // We’ve reached the end of the document. Just close the RuleSet. - return; - } - } - } else { - $oRule = Rule::parse($oParserState); - } - if($oRule) { - $oRuleSet->addRule($oRule); - } - } - $oParserState->consume('}'); - } + /** + * @param int $iLineNo + */ + public function __construct($iLineNo = 0) + { + $this->aRules = []; + $this->iLineNo = $iLineNo; + $this->aComments = []; + } - /** - * @return int - */ - public function getLineNo() { - return $this->iLineNo; - } + /** + * @return void + * + * @throws UnexpectedTokenException + * @throws UnexpectedEOFException + */ + public static function parseRuleSet(ParserState $oParserState, RuleSet $oRuleSet) + { + while ($oParserState->comes(';')) { + $oParserState->consume(';'); + } + while (!$oParserState->comes('}')) { + $oRule = null; + if ($oParserState->getSettings()->bLenientParsing) { + try { + $oRule = Rule::parse($oParserState); + } catch (UnexpectedTokenException $e) { + try { + $sConsume = $oParserState->consumeUntil(["\n", ";", '}'], true); + // We need to “unfind” the matches to the end of the ruleSet as this will be matched later + if ($oParserState->streql(substr($sConsume, -1), '}')) { + $oParserState->backtrack(1); + } else { + while ($oParserState->comes(';')) { + $oParserState->consume(';'); + } + } + } catch (UnexpectedTokenException $e) { + // We’ve reached the end of the document. Just close the RuleSet. + return; + } + } + } else { + $oRule = Rule::parse($oParserState); + } + if ($oRule) { + $oRuleSet->addRule($oRule); + } + } + $oParserState->consume('}'); + } - public function addRule(Rule $oRule, Rule $oSibling = null) { - $sRule = $oRule->getRule(); - if(!isset($this->aRules[$sRule])) { - $this->aRules[$sRule] = array(); - } + /** + * @return int + */ + public function getLineNo() + { + return $this->iLineNo; + } - $iPosition = count($this->aRules[$sRule]); + /** + * @param Rule|null $oSibling + * + * @return void + */ + public function addRule(Rule $oRule, Rule $oSibling = null) + { + $sRule = $oRule->getRule(); + if (!isset($this->aRules[$sRule])) { + $this->aRules[$sRule] = []; + } - if ($oSibling !== null) { - $iSiblingPos = array_search($oSibling, $this->aRules[$sRule], true); - if ($iSiblingPos !== false) { - $iPosition = $iSiblingPos; - } - } + $iPosition = count($this->aRules[$sRule]); - array_splice($this->aRules[$sRule], $iPosition, 0, array($oRule)); - } + if ($oSibling !== null) { + $iSiblingPos = array_search($oSibling, $this->aRules[$sRule], true); + if ($iSiblingPos !== false) { + $iPosition = $iSiblingPos; + $oRule->setPosition($oSibling->getLineNo(), $oSibling->getColNo() - 1); + } + } + if ($oRule->getLineNo() === 0 && $oRule->getColNo() === 0) { + //this node is added manually, give it the next best line + $rules = $this->getRules(); + $pos = count($rules); + if ($pos > 0) { + $last = $rules[$pos - 1]; + $oRule->setPosition($last->getLineNo() + 1, 0); + } + } - /** - * Returns all rules matching the given rule name - * @param (null|string|Rule) $mRule pattern to search for. If null, returns all rules. if the pattern ends with a dash, all rules starting with the pattern are returned as well as one matching the pattern with the dash excluded. passing a Rule behaves like calling getRules($mRule->getRule()). - * @example $oRuleSet->getRules('font-') //returns an array of all rules either beginning with font- or matching font. - * @example $oRuleSet->getRules('font') //returns array(0 => $oRule, …) or array(). - * @return Rule[] Rules. - */ - public function getRules($mRule = null) { - if ($mRule instanceof Rule) { - $mRule = $mRule->getRule(); - } - $aResult = array(); - foreach($this->aRules as $sName => $aRules) { - // Either no search rule is given or the search rule matches the found rule exactly or the search rule ends in “-” and the found rule starts with the search rule. - if(!$mRule || $sName === $mRule || (strrpos($mRule, '-') === strlen($mRule) - strlen('-') && (strpos($sName, $mRule) === 0 || $sName === substr($mRule, 0, -1)))) { - $aResult = array_merge($aResult, $aRules); - } - } - return $aResult; - } + array_splice($this->aRules[$sRule], $iPosition, 0, [$oRule]); + } - /** - * Override all the rules of this set. - * @param Rule[] $aRules The rules to override with. - */ - public function setRules(array $aRules) { - $this->aRules = array(); - foreach ($aRules as $rule) { - $this->addRule($rule); - } - } + /** + * Returns all rules matching the given rule name + * + * @example $oRuleSet->getRules('font') // returns array(0 => $oRule, …) or array(). + * + * @example $oRuleSet->getRules('font-') + * //returns an array of all rules either beginning with font- or matching font. + * + * @param Rule|string|null $mRule + * Pattern to search for. If null, returns all rules. + * If the pattern ends with a dash, all rules starting with the pattern are returned + * as well as one matching the pattern with the dash excluded. + * Passing a Rule behaves like calling `getRules($mRule->getRule())`. + * + * @return array + */ + public function getRules($mRule = null) + { + if ($mRule instanceof Rule) { + $mRule = $mRule->getRule(); + } + /** @var array $aResult */ + $aResult = []; + foreach ($this->aRules as $sName => $aRules) { + // Either no search rule is given or the search rule matches the found rule exactly + // or the search rule ends in “-” and the found rule starts with the search rule. + if ( + !$mRule || $sName === $mRule + || ( + strrpos($mRule, '-') === strlen($mRule) - strlen('-') + && (strpos($sName, $mRule) === 0 || $sName === substr($mRule, 0, -1)) + ) + ) { + $aResult = array_merge($aResult, $aRules); + } + } + usort($aResult, function (Rule $first, Rule $second) { + if ($first->getLineNo() === $second->getLineNo()) { + return $first->getColNo() - $second->getColNo(); + } + return $first->getLineNo() - $second->getLineNo(); + }); + return $aResult; + } - /** - * 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. - * @param (string) $mRule pattern to search for. If null, returns all rules. if the pattern ends with a dash, all rules starting with the pattern are returned as well as one matching the pattern with the dash excluded. passing a Rule behaves like calling getRules($mRule->getRule()). - * 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 @link{getRules()} would yield an indexed array containing both. - * @return Rule[] Rules. - */ - public function getRulesAssoc($mRule = null) { - $aResult = array(); - foreach($this->getRules($mRule) as $oRule) { - $aResult[$oRule->getRule()] = $oRule; - } - return $aResult; - } + /** + * Overrides all the rules of this set. + * + * @param array $aRules The rules to override with. + * + * @return void + */ + public function setRules(array $aRules) + { + $this->aRules = []; + foreach ($aRules as $rule) { + $this->addRule($rule); + } + } - /** - * Remove a rule from this RuleSet. This accepts all the possible values that @link{getRules()} accepts. If given a Rule, it will only remove this particular rule (by identity). If given a name, it will remove all rules by that name. Note: this is different from pre-v.2.0 behaviour of PHP-CSS-Parser, where passing a Rule instance would remove all rules with the same name. To get the old behvaiour, use removeRule($oRule->getRule()). - * @param (null|string|Rule) $mRule pattern to remove. If $mRule is null, all rules are removed. If the pattern ends in a dash, all rules starting with the pattern are removed as well as one matching the pattern with the dash excluded. Passing a Rule behaves matches by identity. - */ - public function removeRule($mRule) { - if($mRule instanceof Rule) { - $sRule = $mRule->getRule(); - if(!isset($this->aRules[$sRule])) { - return; - } - foreach($this->aRules[$sRule] as $iKey => $oRule) { - if($oRule === $mRule) { - unset($this->aRules[$sRule][$iKey]); - } - } - } else { - foreach($this->aRules as $sName => $aRules) { - // Either no search rule is given or the search rule matches the found rule exactly or the search rule ends in “-” and the found rule starts with the search rule or equals it (without the trailing dash). - if(!$mRule || $sName === $mRule || (strrpos($mRule, '-') === strlen($mRule) - strlen('-') && (strpos($sName, $mRule) === 0 || $sName === substr($mRule, 0, -1)))) { - unset($this->aRules[$sName]); - } - } - } - } + /** + * Returns all rules matching the given pattern and returns them in an associative array with the rule’s name + * as keys. This method exists mainly for backwards-compatibility and is really only partially useful. + * + * Note: This method loses some information: Calling this (with an argument of `background-`) on a declaration block + * like `{ background-color: green; background-color; rgba(0, 127, 0, 0.7); }` will only yield an associative array + * containing the rgba-valued rule while `getRules()` would yield an indexed array containing both. + * + * @param Rule|string|null $mRule $mRule + * Pattern to search for. If null, returns all rules. If the pattern ends with a dash, + * all rules starting with the pattern are returned as well as one matching the pattern with the dash + * excluded. Passing a Rule behaves like calling `getRules($mRule->getRule())`. + * + * @return array + */ + public function getRulesAssoc($mRule = null) + { + /** @var array $aResult */ + $aResult = []; + foreach ($this->getRules($mRule) as $oRule) { + $aResult[$oRule->getRule()] = $oRule; + } + return $aResult; + } - public function __toString() { - return $this->render(new \Sabberworm\CSS\OutputFormat()); - } + /** + * Removes a rule from this RuleSet. This accepts all the possible values that `getRules()` accepts. + * + * If given a Rule, it will only remove this particular rule (by identity). + * If given a name, it will remove all rules by that name. + * + * Note: this is different from pre-v.2.0 behaviour of PHP-CSS-Parser, where passing a Rule instance would + * remove all rules with the same name. To get the old behaviour, use `removeRule($oRule->getRule())`. + * + * @param Rule|string|null $mRule + * pattern to remove. If $mRule is null, all rules are removed. If the pattern ends in a dash, + * all rules starting with the pattern are removed as well as one matching the pattern with the dash + * excluded. Passing a Rule behaves matches by identity. + * + * @return void + */ + public function removeRule($mRule) + { + if ($mRule instanceof Rule) { + $sRule = $mRule->getRule(); + if (!isset($this->aRules[$sRule])) { + return; + } + foreach ($this->aRules[$sRule] as $iKey => $oRule) { + if ($oRule === $mRule) { + unset($this->aRules[$sRule][$iKey]); + } + } + } else { + foreach ($this->aRules as $sName => $aRules) { + // Either no search rule is given or the search rule matches the found rule exactly + // or the search rule ends in “-” and the found rule starts with the search rule or equals it + // (without the trailing dash). + if ( + !$mRule || $sName === $mRule + || (strrpos($mRule, '-') === strlen($mRule) - strlen('-') + && (strpos($sName, $mRule) === 0 || $sName === substr($mRule, 0, -1))) + ) { + unset($this->aRules[$sName]); + } + } + } + } - public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) { - $sResult = ''; - $bIsFirst = true; - foreach ($this->aRules as $aRules) { - foreach($aRules as $oRule) { - $sRendered = $oOutputFormat->safely(function() use ($oRule, $oOutputFormat) { - return $oRule->render($oOutputFormat->nextLevel()); - }); - if($sRendered === null) { - continue; - } - if($bIsFirst) { - $bIsFirst = false; - $sResult .= $oOutputFormat->nextLevel()->spaceBeforeRules(); - } else { - $sResult .= $oOutputFormat->nextLevel()->spaceBetweenRules(); - } - $sResult .= $sRendered; - } - } - - if(!$bIsFirst) { - // Had some output - $sResult .= $oOutputFormat->spaceAfterRules(); - } + /** + * @return string + */ + public function __toString() + { + return $this->render(new OutputFormat()); + } - return $oOutputFormat->removeLastSemicolon($sResult); - } + /** + * @return string + */ + public function render(OutputFormat $oOutputFormat) + { + $sResult = ''; + $bIsFirst = true; + foreach ($this->aRules as $aRules) { + foreach ($aRules as $oRule) { + $sRendered = $oOutputFormat->safely(function () use ($oRule, $oOutputFormat) { + return $oRule->render($oOutputFormat->nextLevel()); + }); + if ($sRendered === null) { + continue; + } + if ($bIsFirst) { + $bIsFirst = false; + $sResult .= $oOutputFormat->nextLevel()->spaceBeforeRules(); + } else { + $sResult .= $oOutputFormat->nextLevel()->spaceBetweenRules(); + } + $sResult .= $sRendered; + } + } - /** - * @param array $aComments Array of comments. - */ - public function addComments(array $aComments) { - $this->aComments = array_merge($this->aComments, $aComments); - } + if (!$bIsFirst) { + // Had some output + $sResult .= $oOutputFormat->spaceAfterRules(); + } - /** - * @return array - */ - public function getComments() { - return $this->aComments; - } + return $oOutputFormat->removeLastSemicolon($sResult); + } - /** - * @param array $aComments Array containing Comment objects. - */ - public function setComments(array $aComments) { - $this->aComments = $aComments; - } + /** + * @param array $aComments + * + * @return void + */ + public function addComments(array $aComments) + { + $this->aComments = array_merge($this->aComments, $aComments); + } + /** + * @return array + */ + public function getComments() + { + return $this->aComments; + } + + /** + * @param array $aComments + * + * @return void + */ + public function setComments(array $aComments) + { + $this->aComments = $aComments; + } } diff --git a/lib/php-css-parser/Settings.php b/lib/php-css-parser/Settings.php index cb89a8636ba..7b8580962c9 100644 --- a/lib/php-css-parser/Settings.php +++ b/lib/php-css-parser/Settings.php @@ -2,53 +2,88 @@ namespace Sabberworm\CSS; -use Sabberworm\CSS\Rule\Rule; - /** * Parser settings class. * * Configure parser behaviour here. */ -class Settings { - /** - * Multi-byte string support. If true (mbstring extension must be enabled), will use (slower) mb_strlen, mb_convert_case, mb_substr and mb_strpos functions. Otherwise, the normal (ASCII-Only) functions will be used. - */ - public $bMultibyteSupport; +class Settings +{ + /** + * Multi-byte string support. + * If true (mbstring extension must be enabled), will use (slower) `mb_strlen`, `mb_convert_case`, `mb_substr` + * and `mb_strpos` functions. Otherwise, the normal (ASCII-Only) functions will be used. + * + * @var bool + */ + public $bMultibyteSupport; - /** - * The default charset for the CSS if no `@charset` rule is found. Defaults to utf-8. - */ - public $sDefaultCharset = 'utf-8'; + /** + * The default charset for the CSS if no `@charset` rule is found. Defaults to utf-8. + * + * @var string + */ + public $sDefaultCharset = 'utf-8'; - /** - * Lenient parsing. When used (which is true by default), the parser will not choke on unexpected tokens but simply ignore them. - */ - public $bLenientParsing = true; + /** + * Lenient parsing. When used (which is true by default), the parser will not choke + * on unexpected tokens but simply ignore them. + * + * @var bool + */ + public $bLenientParsing = true; - private function __construct() { - $this->bMultibyteSupport = extension_loaded('mbstring'); - } + private function __construct() + { + $this->bMultibyteSupport = extension_loaded('mbstring'); + } - public static function create() { - return new Settings(); - } - - public function withMultibyteSupport($bMultibyteSupport = true) { - $this->bMultibyteSupport = $bMultibyteSupport; - return $this; - } - - public function withDefaultCharset($sDefaultCharset) { - $this->sDefaultCharset = $sDefaultCharset; - return $this; - } - - public function withLenientParsing($bLenientParsing = true) { - $this->bLenientParsing = $bLenientParsing; - return $this; - } - - public function beStrict() { - return $this->withLenientParsing(false); - } -} \ No newline at end of file + /** + * @return self new instance + */ + public static function create() + { + return new Settings(); + } + + /** + * @param bool $bMultibyteSupport + * + * @return self fluent interface + */ + public function withMultibyteSupport($bMultibyteSupport = true) + { + $this->bMultibyteSupport = $bMultibyteSupport; + return $this; + } + + /** + * @param string $sDefaultCharset + * + * @return self fluent interface + */ + public function withDefaultCharset($sDefaultCharset) + { + $this->sDefaultCharset = $sDefaultCharset; + return $this; + } + + /** + * @param bool $bLenientParsing + * + * @return self fluent interface + */ + public function withLenientParsing($bLenientParsing = true) + { + $this->bLenientParsing = $bLenientParsing; + return $this; + } + + /** + * @return self fluent interface + */ + public function beStrict() + { + return $this->withLenientParsing(false); + } +} diff --git a/lib/php-css-parser/Value/CSSFunction.php b/lib/php-css-parser/Value/CSSFunction.php index 941df236977..e6b8c1189f4 100644 --- a/lib/php-css-parser/Value/CSSFunction.php +++ b/lib/php-css-parser/Value/CSSFunction.php @@ -2,39 +2,72 @@ namespace Sabberworm\CSS\Value; -class CSSFunction extends ValueList { +use Sabberworm\CSS\OutputFormat; - protected $sName; +class CSSFunction extends ValueList +{ + /** + * @var string + */ + protected $sName; - public function __construct($sName, $aArguments, $sSeparator = ',', $iLineNo = 0) { - if($aArguments instanceof RuleValueList) { - $sSeparator = $aArguments->getListSeparator(); - $aArguments = $aArguments->getListComponents(); - } - $this->sName = $sName; - $this->iLineNo = $iLineNo; - parent::__construct($aArguments, $sSeparator, $iLineNo); - } + /** + * @param string $sName + * @param RuleValueList|array $aArguments + * @param string $sSeparator + * @param int $iLineNo + */ + public function __construct($sName, $aArguments, $sSeparator = ',', $iLineNo = 0) + { + if ($aArguments instanceof RuleValueList) { + $sSeparator = $aArguments->getListSeparator(); + $aArguments = $aArguments->getListComponents(); + } + $this->sName = $sName; + $this->iLineNo = $iLineNo; + parent::__construct($aArguments, $sSeparator, $iLineNo); + } - public function getName() { - return $this->sName; - } + /** + * @return string + */ + public function getName() + { + return $this->sName; + } - public function setName($sName) { - $this->sName = $sName; - } + /** + * @param string $sName + * + * @return void + */ + public function setName($sName) + { + $this->sName = $sName; + } - public function getArguments() { - return $this->aComponents; - } + /** + * @return array + */ + public function getArguments() + { + return $this->aComponents; + } - public function __toString() { - return $this->render(new \Sabberworm\CSS\OutputFormat()); - } - - public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) { - $aArguments = parent::render($oOutputFormat); - return "{$this->sName}({$aArguments})"; - } + /** + * @return string + */ + public function __toString() + { + return $this->render(new OutputFormat()); + } + /** + * @return string + */ + public function render(OutputFormat $oOutputFormat) + { + $aArguments = parent::render($oOutputFormat); + return "{$this->sName}({$aArguments})"; + } } diff --git a/lib/php-css-parser/Value/CSSString.php b/lib/php-css-parser/Value/CSSString.php index 9f9c050e97c..9fafedd7a46 100644 --- a/lib/php-css-parser/Value/CSSString.php +++ b/lib/php-css-parser/Value/CSSString.php @@ -2,65 +2,104 @@ namespace Sabberworm\CSS\Value; +use Sabberworm\CSS\OutputFormat; use Sabberworm\CSS\Parsing\ParserState; use Sabberworm\CSS\Parsing\SourceException; +use Sabberworm\CSS\Parsing\UnexpectedEOFException; +use Sabberworm\CSS\Parsing\UnexpectedTokenException; -class CSSString extends PrimitiveValue { +class CSSString extends PrimitiveValue +{ + /** + * @var string + */ + private $sString; - private $sString; + /** + * @param string $sString + * @param int $iLineNo + */ + public function __construct($sString, $iLineNo = 0) + { + $this->sString = $sString; + parent::__construct($iLineNo); + } - public function __construct($sString, $iLineNo = 0) { - $this->sString = $sString; - parent::__construct($iLineNo); - } + /** + * @return CSSString + * + * @throws SourceException + * @throws UnexpectedEOFException + * @throws UnexpectedTokenException + */ + public static function parse(ParserState $oParserState) + { + $sBegin = $oParserState->peek(); + $sQuote = null; + if ($sBegin === "'") { + $sQuote = "'"; + } elseif ($sBegin === '"') { + $sQuote = '"'; + } + if ($sQuote !== null) { + $oParserState->consume($sQuote); + } + $sResult = ""; + $sContent = null; + if ($sQuote === null) { + // Unquoted strings end in whitespace or with braces, brackets, parentheses + while (!preg_match('/[\\s{}()<>\\[\\]]/isu', $oParserState->peek())) { + $sResult .= $oParserState->parseCharacter(false); + } + } else { + while (!$oParserState->comes($sQuote)) { + $sContent = $oParserState->parseCharacter(false); + if ($sContent === null) { + throw new SourceException( + "Non-well-formed quoted string {$oParserState->peek(3)}", + $oParserState->currentLine() + ); + } + $sResult .= $sContent; + } + $oParserState->consume($sQuote); + } + return new CSSString($sResult, $oParserState->currentLine()); + } - public static function parse(ParserState $oParserState) { - $sBegin = $oParserState->peek(); - $sQuote = null; - if ($sBegin === "'") { - $sQuote = "'"; - } else if ($sBegin === '"') { - $sQuote = '"'; - } - if ($sQuote !== null) { - $oParserState->consume($sQuote); - } - $sResult = ""; - $sContent = null; - if ($sQuote === null) { - // Unquoted strings end in whitespace or with braces, brackets, parentheses - while (!preg_match('/[\\s{}()<>\\[\\]]/isu', $oParserState->peek())) { - $sResult .= $oParserState->parseCharacter(false); - } - } else { - while (!$oParserState->comes($sQuote)) { - $sContent = $oParserState->parseCharacter(false); - if ($sContent === null) { - throw new SourceException("Non-well-formed quoted string {$oParserState->peek(3)}", $oParserState->currentLine()); - } - $sResult .= $sContent; - } - $oParserState->consume($sQuote); - } - return new CSSString($sResult, $oParserState->currentLine()); - } + /** + * @param string $sString + * + * @return void + */ + public function setString($sString) + { + $this->sString = $sString; + } - public function setString($sString) { - $this->sString = $sString; - } + /** + * @return string + */ + public function getString() + { + return $this->sString; + } - public function getString() { - return $this->sString; - } + /** + * @return string + */ + public function __toString() + { + return $this->render(new OutputFormat()); + } - public function __toString() { - return $this->render(new \Sabberworm\CSS\OutputFormat()); - } - - public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) { - $sString = addslashes($this->sString); - $sString = str_replace("\n", '\A', $sString); - return $oOutputFormat->getStringQuotingType() . $sString . $oOutputFormat->getStringQuotingType(); - } - -} \ No newline at end of file + /** + * @return string + */ + public function render(OutputFormat $oOutputFormat) + { + $sString = addslashes($this->sString); + $sString = str_replace("\n", '\A', $sString); + return $oOutputFormat->getStringQuotingType() . $sString . $oOutputFormat->getStringQuotingType(); + } +} diff --git a/lib/php-css-parser/Value/CalcFunction.php b/lib/php-css-parser/Value/CalcFunction.php index 92475209db0..5c92e0c089b 100644 --- a/lib/php-css-parser/Value/CalcFunction.php +++ b/lib/php-css-parser/Value/CalcFunction.php @@ -3,60 +3,87 @@ namespace Sabberworm\CSS\Value; use Sabberworm\CSS\Parsing\ParserState; +use Sabberworm\CSS\Parsing\UnexpectedEOFException; use Sabberworm\CSS\Parsing\UnexpectedTokenException; -class CalcFunction extends CSSFunction { - const T_OPERAND = 1; - const T_OPERATOR = 2; +class CalcFunction extends CSSFunction +{ + /** + * @var int + */ + const T_OPERAND = 1; - public static function parse(ParserState $oParserState) { - $aOperators = array('+', '-', '*', '/'); - $sFunction = trim($oParserState->consumeUntil('(', false, true)); - $oCalcList = new CalcRuleValueList($oParserState->currentLine()); - $oList = new RuleValueList(',', $oParserState->currentLine()); - $iNestingLevel = 0; - $iLastComponentType = NULL; - while(!$oParserState->comes(')') || $iNestingLevel > 0) { - $oParserState->consumeWhiteSpace(); - if ($oParserState->comes('(')) { - $iNestingLevel++; - $oCalcList->addListComponent($oParserState->consume(1)); - continue; - } else if ($oParserState->comes(')')) { - $iNestingLevel--; - $oCalcList->addListComponent($oParserState->consume(1)); - continue; - } - if ($iLastComponentType != CalcFunction::T_OPERAND) { - $oVal = Value::parsePrimitiveValue($oParserState); - $oCalcList->addListComponent($oVal); - $iLastComponentType = CalcFunction::T_OPERAND; - } else { - if (in_array($oParserState->peek(), $aOperators)) { - if (($oParserState->comes('-') || $oParserState->comes('+'))) { - if ($oParserState->peek(1, -1) != ' ' || !($oParserState->comes('- ') || $oParserState->comes('+ '))) { - throw new UnexpectedTokenException(" {$oParserState->peek()} ", $oParserState->peek(1, -1) . $oParserState->peek(2), 'literal', $oParserState->currentLine()); - } - } - $oCalcList->addListComponent($oParserState->consume(1)); - $iLastComponentType = CalcFunction::T_OPERATOR; - } else { - throw new UnexpectedTokenException( - sprintf( - 'Next token was expected to be an operand of type %s. Instead "%s" was found.', - implode(', ', $aOperators), - $oVal - ), - '', - 'custom', - $oParserState->currentLine() - ); - } - } - } - $oList->addListComponent($oCalcList); - $oParserState->consume(')'); - return new CalcFunction($sFunction, $oList, ',', $oParserState->currentLine()); - } + /** + * @var int + */ + const T_OPERATOR = 2; + /** + * @return CalcFunction + * + * @throws UnexpectedTokenException + * @throws UnexpectedEOFException + */ + public static function parse(ParserState $oParserState) + { + $aOperators = ['+', '-', '*', '/']; + $sFunction = trim($oParserState->consumeUntil('(', false, true)); + $oCalcList = new CalcRuleValueList($oParserState->currentLine()); + $oList = new RuleValueList(',', $oParserState->currentLine()); + $iNestingLevel = 0; + $iLastComponentType = null; + while (!$oParserState->comes(')') || $iNestingLevel > 0) { + $oParserState->consumeWhiteSpace(); + if ($oParserState->comes('(')) { + $iNestingLevel++; + $oCalcList->addListComponent($oParserState->consume(1)); + $oParserState->consumeWhiteSpace(); + continue; + } elseif ($oParserState->comes(')')) { + $iNestingLevel--; + $oCalcList->addListComponent($oParserState->consume(1)); + $oParserState->consumeWhiteSpace(); + continue; + } + if ($iLastComponentType != CalcFunction::T_OPERAND) { + $oVal = Value::parsePrimitiveValue($oParserState); + $oCalcList->addListComponent($oVal); + $iLastComponentType = CalcFunction::T_OPERAND; + } else { + if (in_array($oParserState->peek(), $aOperators)) { + if (($oParserState->comes('-') || $oParserState->comes('+'))) { + if ( + $oParserState->peek(1, -1) != ' ' + || !($oParserState->comes('- ') + || $oParserState->comes('+ ')) + ) { + throw new UnexpectedTokenException( + " {$oParserState->peek()} ", + $oParserState->peek(1, -1) . $oParserState->peek(2), + 'literal', + $oParserState->currentLine() + ); + } + } + $oCalcList->addListComponent($oParserState->consume(1)); + $iLastComponentType = CalcFunction::T_OPERATOR; + } else { + throw new UnexpectedTokenException( + sprintf( + 'Next token was expected to be an operand of type %s. Instead "%s" was found.', + implode(', ', $aOperators), + $oVal + ), + '', + 'custom', + $oParserState->currentLine() + ); + } + } + $oParserState->consumeWhiteSpace(); + } + $oList->addListComponent($oCalcList); + $oParserState->consume(')'); + return new CalcFunction($sFunction, $oList, ',', $oParserState->currentLine()); + } } diff --git a/lib/php-css-parser/Value/CalcRuleValueList.php b/lib/php-css-parser/Value/CalcRuleValueList.php index bde8a9d368a..7dbd26a1b4d 100644 --- a/lib/php-css-parser/Value/CalcRuleValueList.php +++ b/lib/php-css-parser/Value/CalcRuleValueList.php @@ -2,13 +2,23 @@ namespace Sabberworm\CSS\Value; -class CalcRuleValueList extends RuleValueList { - public function __construct($iLineNo = 0) { - parent::__construct(array(), ',', $iLineNo); - } +use Sabberworm\CSS\OutputFormat; - public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) { - return $oOutputFormat->implode(' ', $this->aComponents); - } +class CalcRuleValueList extends RuleValueList +{ + /** + * @param int $iLineNo + */ + public function __construct($iLineNo = 0) + { + parent::__construct(',', $iLineNo); + } + /** + * @return string + */ + public function render(OutputFormat $oOutputFormat) + { + return $oOutputFormat->implode(' ', $this->aComponents); + } } diff --git a/lib/php-css-parser/Value/Color.php b/lib/php-css-parser/Value/Color.php index c6ed9b18b16..8dc52960cd6 100644 --- a/lib/php-css-parser/Value/Color.php +++ b/lib/php-css-parser/Value/Color.php @@ -2,94 +2,165 @@ namespace Sabberworm\CSS\Value; +use Sabberworm\CSS\OutputFormat; use Sabberworm\CSS\Parsing\ParserState; +use Sabberworm\CSS\Parsing\UnexpectedEOFException; +use Sabberworm\CSS\Parsing\UnexpectedTokenException; -class Color extends CSSFunction { +class Color extends CSSFunction +{ + /** + * @param array $aColor + * @param int $iLineNo + */ + public function __construct(array $aColor, $iLineNo = 0) + { + parent::__construct(implode('', array_keys($aColor)), $aColor, ',', $iLineNo); + } - public function __construct($aColor, $iLineNo = 0) { - parent::__construct(implode('', array_keys($aColor)), $aColor, ',', $iLineNo); - } + /** + * @return Color|CSSFunction + * + * @throws UnexpectedEOFException + * @throws UnexpectedTokenException + */ + public static function parse(ParserState $oParserState) + { + $aColor = []; + if ($oParserState->comes('#')) { + $oParserState->consume('#'); + $sValue = $oParserState->parseIdentifier(false); + if ($oParserState->strlen($sValue) === 3) { + $sValue = $sValue[0] . $sValue[0] . $sValue[1] . $sValue[1] . $sValue[2] . $sValue[2]; + } elseif ($oParserState->strlen($sValue) === 4) { + $sValue = $sValue[0] . $sValue[0] . $sValue[1] . $sValue[1] . $sValue[2] . $sValue[2] . $sValue[3] + . $sValue[3]; + } - public static function parse(ParserState $oParserState) { - $aColor = array(); - 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]; - } else if ($oParserState->strlen($sValue) === 4) { - $sValue = $sValue[0] . $sValue[0] . $sValue[1] . $sValue[1] . $sValue[2] . $sValue[2] . $sValue[3] . $sValue[3]; - } + if ($oParserState->strlen($sValue) === 8) { + $aColor = [ + 'r' => new Size(intval($sValue[0] . $sValue[1], 16), null, true, $oParserState->currentLine()), + 'g' => new Size(intval($sValue[2] . $sValue[3], 16), null, true, $oParserState->currentLine()), + 'b' => new Size(intval($sValue[4] . $sValue[5], 16), null, true, $oParserState->currentLine()), + 'a' => new Size( + round(self::mapRange(intval($sValue[6] . $sValue[7], 16), 0, 255, 0, 1), 2), + null, + true, + $oParserState->currentLine() + ), + ]; + } else { + $aColor = [ + 'r' => new Size(intval($sValue[0] . $sValue[1], 16), null, true, $oParserState->currentLine()), + 'g' => new Size(intval($sValue[2] . $sValue[3], 16), null, true, $oParserState->currentLine()), + 'b' => new Size(intval($sValue[4] . $sValue[5], 16), null, true, $oParserState->currentLine()), + ]; + } + } else { + $sColorMode = $oParserState->parseIdentifier(true); + $oParserState->consumeWhiteSpace(); + $oParserState->consume('('); - if ($oParserState->strlen($sValue) === 8) { - $aColor = array( - 'r' => new Size(intval($sValue[0] . $sValue[1], 16), null, true, $oParserState->currentLine()), - 'g' => new Size(intval($sValue[2] . $sValue[3], 16), null, true, $oParserState->currentLine()), - 'b' => new Size(intval($sValue[4] . $sValue[5], 16), null, true, $oParserState->currentLine()), - 'a' => new Size(round(self::mapRange(intval($sValue[6] . $sValue[7], 16), 0, 255, 0, 1), 2), null, true, $oParserState->currentLine()) - ); - } else { - $aColor = array( - 'r' => new Size(intval($sValue[0] . $sValue[1], 16), null, true, $oParserState->currentLine()), - 'g' => new Size(intval($sValue[2] . $sValue[3], 16), null, true, $oParserState->currentLine()), - 'b' => new Size(intval($sValue[4] . $sValue[5], 16), null, true, $oParserState->currentLine()) - ); - } - } else { - $sColorMode = $oParserState->parseIdentifier(true); - $oParserState->consumeWhiteSpace(); - $oParserState->consume('('); - $iLength = $oParserState->strlen($sColorMode); - for ($i = 0; $i < $iLength; ++$i) { - $oParserState->consumeWhiteSpace(); - $aColor[$sColorMode[$i]] = Size::parse($oParserState, true); - $oParserState->consumeWhiteSpace(); - if ($i < ($iLength - 1)) { - $oParserState->consume(','); - } - } - $oParserState->consume(')'); - } - return new Color($aColor, $oParserState->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); + } - private static function mapRange($fVal, $fFromMin, $fFromMax, $fToMin, $fToMax) { - $fFromRange = $fFromMax - $fFromMin; - $fToRange = $fToMax - $fToMin; - $fMultiplier = $fToRange / $fFromRange; - $fNewVal = $fVal - $fFromMin; - $fNewVal *= $fMultiplier; - return $fNewVal + $fToMin; - } + if ($bContainsVar && $oParserState->comes(')')) { + // With a var argument the function can have fewer arguments + break; + } - public function getColor() { - return $this->aComponents; - } + $oParserState->consumeWhiteSpace(); + if ($i < ($iLength - 1)) { + $oParserState->consume(','); + } + } + $oParserState->consume(')'); - public function setColor($aColor) { - $this->setName(implode('', array_keys($aColor))); - $this->aComponents = $aColor; - } + if ($bContainsVar) { + return new CSSFunction($sColorMode, array_values($aColor), ',', $oParserState->currentLine()); + } + } + return new Color($aColor, $oParserState->currentLine()); + } - public function getColorDescription() { - return $this->getName(); - } + /** + * @param float $fVal + * @param float $fFromMin + * @param float $fFromMax + * @param float $fToMin + * @param float $fToMax + * + * @return float + */ + private static function mapRange($fVal, $fFromMin, $fFromMax, $fToMin, $fToMax) + { + $fFromRange = $fFromMax - $fFromMin; + $fToRange = $fToMax - $fToMin; + $fMultiplier = $fToRange / $fFromRange; + $fNewVal = $fVal - $fFromMin; + $fNewVal *= $fMultiplier; + return $fNewVal + $fToMin; + } - public function __toString() { - return $this->render(new \Sabberworm\CSS\OutputFormat()); - } + /** + * @return array + */ + public function getColor() + { + return $this->aComponents; + } - public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) { - // Shorthand RGB color values - if($oOutputFormat->getRGBHashNotation() && implode('', array_keys($this->aComponents)) === 'rgb') { - $sResult = sprintf( - '%02x%02x%02x', - $this->aComponents['r']->getSize(), - $this->aComponents['g']->getSize(), - $this->aComponents['b']->getSize() - ); - return '#'.(($sResult[0] == $sResult[1]) && ($sResult[2] == $sResult[3]) && ($sResult[4] == $sResult[5]) ? "$sResult[0]$sResult[2]$sResult[4]" : $sResult); - } - return parent::render($oOutputFormat); - } + /** + * @param array $aColor + * + * @return void + */ + public function setColor(array $aColor) + { + $this->setName(implode('', array_keys($aColor))); + $this->aComponents = $aColor; + } + + /** + * @return string + */ + public function getColorDescription() + { + return $this->getName(); + } + + /** + * @return string + */ + public function __toString() + { + return $this->render(new OutputFormat()); + } + + /** + * @return string + */ + public function render(OutputFormat $oOutputFormat) + { + // Shorthand RGB color values + if ($oOutputFormat->getRGBHashNotation() && implode('', array_keys($this->aComponents)) === 'rgb') { + $sResult = sprintf( + '%02x%02x%02x', + $this->aComponents['r']->getSize(), + $this->aComponents['g']->getSize(), + $this->aComponents['b']->getSize() + ); + return '#' . (($sResult[0] == $sResult[1]) && ($sResult[2] == $sResult[3]) && ($sResult[4] == $sResult[5]) + ? "$sResult[0]$sResult[2]$sResult[4]" : $sResult); + } + return parent::render($oOutputFormat); + } } diff --git a/lib/php-css-parser/Value/LineName.php b/lib/php-css-parser/Value/LineName.php index eb7392d7f98..e231ce38ff7 100644 --- a/lib/php-css-parser/Value/LineName.php +++ b/lib/php-css-parser/Value/LineName.php @@ -2,40 +2,64 @@ namespace Sabberworm\CSS\Value; +use Sabberworm\CSS\OutputFormat; use Sabberworm\CSS\Parsing\ParserState; +use Sabberworm\CSS\Parsing\UnexpectedEOFException; use Sabberworm\CSS\Parsing\UnexpectedTokenException; -class LineName extends ValueList { - public function __construct($aComponents = array(), $iLineNo = 0) { - parent::__construct($aComponents, ' ', $iLineNo); - } +class LineName extends ValueList +{ + /** + * @param array $aComponents + * @param int $iLineNo + */ + public function __construct(array $aComponents = [], $iLineNo = 0) + { + parent::__construct($aComponents, ' ', $iLineNo); + } - public static function parse(ParserState $oParserState) { - $oParserState->consume('['); - $oParserState->consumeWhiteSpace(); - $aNames = array(); - do { - if($oParserState->getSettings()->bLenientParsing) { - try { - $aNames[] = $oParserState->parseIdentifier(); - } catch(UnexpectedTokenException $e) {} - } else { - $aNames[] = $oParserState->parseIdentifier(); - } - $oParserState->consumeWhiteSpace(); - } while (!$oParserState->comes(']')); - $oParserState->consume(']'); - return new LineName($aNames, $oParserState->currentLine()); - } + /** + * @return LineName + * + * @throws UnexpectedTokenException + * @throws UnexpectedEOFException + */ + public static function parse(ParserState $oParserState) + { + $oParserState->consume('['); + $oParserState->consumeWhiteSpace(); + $aNames = []; + do { + if ($oParserState->getSettings()->bLenientParsing) { + try { + $aNames[] = $oParserState->parseIdentifier(); + } catch (UnexpectedTokenException $e) { + if (!$oParserState->comes(']')) { + throw $e; + } + } + } else { + $aNames[] = $oParserState->parseIdentifier(); + } + $oParserState->consumeWhiteSpace(); + } while (!$oParserState->comes(']')); + $oParserState->consume(']'); + return new LineName($aNames, $oParserState->currentLine()); + } + /** + * @return string + */ + public function __toString() + { + return $this->render(new OutputFormat()); + } - - public function __toString() { - return $this->render(new \Sabberworm\CSS\OutputFormat()); - } - - public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) { - return '[' . parent::render(\Sabberworm\CSS\OutputFormat::createCompact()) . ']'; - } - + /** + * @return string + */ + public function render(OutputFormat $oOutputFormat) + { + return '[' . parent::render(OutputFormat::createCompact()) . ']'; + } } diff --git a/lib/php-css-parser/Value/PrimitiveValue.php b/lib/php-css-parser/Value/PrimitiveValue.php index 187ce7e6a01..055a439751e 100644 --- a/lib/php-css-parser/Value/PrimitiveValue.php +++ b/lib/php-css-parser/Value/PrimitiveValue.php @@ -2,9 +2,13 @@ namespace Sabberworm\CSS\Value; -abstract class PrimitiveValue extends Value { - public function __construct($iLineNo = 0) { +abstract class PrimitiveValue extends Value +{ + /** + * @param int $iLineNo + */ + public function __construct($iLineNo = 0) + { parent::__construct($iLineNo); } - -} \ No newline at end of file +} diff --git a/lib/php-css-parser/Value/RuleValueList.php b/lib/php-css-parser/Value/RuleValueList.php index d89c326caf3..5d533a7cbe0 100644 --- a/lib/php-css-parser/Value/RuleValueList.php +++ b/lib/php-css-parser/Value/RuleValueList.php @@ -2,8 +2,14 @@ namespace Sabberworm\CSS\Value; -class RuleValueList extends ValueList { - public function __construct($sSeparator = ',', $iLineNo = 0) { - parent::__construct(array(), $sSeparator, $iLineNo); - } -} \ No newline at end of file +class RuleValueList extends ValueList +{ + /** + * @param string $sSeparator + * @param int $iLineNo + */ + public function __construct($sSeparator = ',', $iLineNo = 0) + { + parent::__construct([], $sSeparator, $iLineNo); + } +} diff --git a/lib/php-css-parser/Value/Size.php b/lib/php-css-parser/Value/Size.php index f65246b51f7..b3801dc17dc 100644 --- a/lib/php-css-parser/Value/Size.php +++ b/lib/php-css-parser/Value/Size.php @@ -2,121 +2,208 @@ namespace Sabberworm\CSS\Value; +use Sabberworm\CSS\OutputFormat; use Sabberworm\CSS\Parsing\ParserState; +use Sabberworm\CSS\Parsing\UnexpectedEOFException; +use Sabberworm\CSS\Parsing\UnexpectedTokenException; -class Size extends PrimitiveValue { +class Size extends PrimitiveValue +{ + /** + * vh/vw/vm(ax)/vmin/rem are absolute insofar as they don’t scale to the immediate parent (only the viewport) + * + * @var array + */ + const ABSOLUTE_SIZE_UNITS = ['px', 'cm', 'mm', 'mozmm', 'in', 'pt', 'pc', 'vh', 'vw', 'vmin', 'vmax', 'rem']; - const ABSOLUTE_SIZE_UNITS = 'px/cm/mm/mozmm/in/pt/pc/vh/vw/vm/vmin/vmax/rem'; //vh/vw/vm(ax)/vmin/rem are absolute insofar as they don’t scale to the immediate parent (only the viewport) - const RELATIVE_SIZE_UNITS = '%/em/ex/ch/fr'; - const NON_SIZE_UNITS = 'deg/grad/rad/s/ms/turns/Hz/kHz'; + /** + * @var array + */ + const RELATIVE_SIZE_UNITS = ['%', 'em', 'ex', 'ch', 'fr']; - private static $SIZE_UNITS = null; + /** + * @var array + */ + const NON_SIZE_UNITS = ['deg', 'grad', 'rad', 's', 'ms', 'turns', 'Hz', 'kHz']; - private $fSize; - private $sUnit; - private $bIsColorComponent; + /** + * @var array>|null + */ + private static $SIZE_UNITS = null; - public function __construct($fSize, $sUnit = null, $bIsColorComponent = false, $iLineNo = 0) { - parent::__construct($iLineNo); - $this->fSize = floatval($fSize); - $this->sUnit = $sUnit; - $this->bIsColorComponent = $bIsColorComponent; - } + /** + * @var float + */ + private $fSize; - public static function parse(ParserState $oParserState, $bIsColorComponent = false) { - $sSize = ''; - if ($oParserState->comes('-')) { - $sSize .= $oParserState->consume('-'); - } - while (is_numeric($oParserState->peek()) || $oParserState->comes('.')) { - if ($oParserState->comes('.')) { - $sSize .= $oParserState->consume('.'); - } else { - $sSize .= $oParserState->consume(1); - } - } + /** + * @var string|null + */ + private $sUnit; - $sUnit = null; - $aSizeUnits = self::getSizeUnits(); - foreach($aSizeUnits as $iLength => &$aValues) { - $sKey = strtolower($oParserState->peek($iLength)); - if(array_key_exists($sKey, $aValues)) { - if (($sUnit = $aValues[$sKey]) !== null) { - $oParserState->consume($iLength); - break; - } - } - } - return new Size(floatval($sSize), $sUnit, $bIsColorComponent, $oParserState->currentLine()); - } + /** + * @var bool + */ + private $bIsColorComponent; - private static function getSizeUnits() { - if(self::$SIZE_UNITS === null) { - self::$SIZE_UNITS = array(); - foreach (explode('/', Size::ABSOLUTE_SIZE_UNITS.'/'.Size::RELATIVE_SIZE_UNITS.'/'.Size::NON_SIZE_UNITS) as $val) { - $iSize = strlen($val); - if(!isset(self::$SIZE_UNITS[$iSize])) { - self::$SIZE_UNITS[$iSize] = array(); - } - self::$SIZE_UNITS[$iSize][strtolower($val)] = $val; - } + /** + * @param float|int|string $fSize + * @param string|null $sUnit + * @param bool $bIsColorComponent + * @param int $iLineNo + */ + public function __construct($fSize, $sUnit = null, $bIsColorComponent = false, $iLineNo = 0) + { + parent::__construct($iLineNo); + $this->fSize = (float)$fSize; + $this->sUnit = $sUnit; + $this->bIsColorComponent = $bIsColorComponent; + } - // FIXME: Should we not order the longest units first? - ksort(self::$SIZE_UNITS, SORT_NUMERIC); - } + /** + * @param bool $bIsColorComponent + * + * @return Size + * + * @throws UnexpectedEOFException + * @throws UnexpectedTokenException + */ + public static function parse(ParserState $oParserState, $bIsColorComponent = false) + { + $sSize = ''; + if ($oParserState->comes('-')) { + $sSize .= $oParserState->consume('-'); + } + while (is_numeric($oParserState->peek()) || $oParserState->comes('.')) { + if ($oParserState->comes('.')) { + $sSize .= $oParserState->consume('.'); + } else { + $sSize .= $oParserState->consume(1); + } + } - return self::$SIZE_UNITS; - } + $sUnit = null; + $aSizeUnits = self::getSizeUnits(); + foreach ($aSizeUnits as $iLength => &$aValues) { + $sKey = strtolower($oParserState->peek($iLength)); + if (array_key_exists($sKey, $aValues)) { + if (($sUnit = $aValues[$sKey]) !== null) { + $oParserState->consume($iLength); + break; + } + } + } + return new Size((float)$sSize, $sUnit, $bIsColorComponent, $oParserState->currentLine()); + } - public function setUnit($sUnit) { - $this->sUnit = $sUnit; - } + /** + * @return array> + */ + private static function getSizeUnits() + { + if (!is_array(self::$SIZE_UNITS)) { + self::$SIZE_UNITS = []; + foreach (array_merge(self::ABSOLUTE_SIZE_UNITS, self::RELATIVE_SIZE_UNITS, self::NON_SIZE_UNITS) as $val) { + $iSize = strlen($val); + if (!isset(self::$SIZE_UNITS[$iSize])) { + self::$SIZE_UNITS[$iSize] = []; + } + self::$SIZE_UNITS[$iSize][strtolower($val)] = $val; + } - public function getUnit() { - return $this->sUnit; - } + krsort(self::$SIZE_UNITS, SORT_NUMERIC); + } - public function setSize($fSize) { - $this->fSize = floatval($fSize); - } + return self::$SIZE_UNITS; + } - public function getSize() { - return $this->fSize; - } + /** + * @param string $sUnit + * + * @return void + */ + public function setUnit($sUnit) + { + $this->sUnit = $sUnit; + } - public function isColorComponent() { - return $this->bIsColorComponent; - } + /** + * @return string|null + */ + public function getUnit() + { + return $this->sUnit; + } - /** - * Returns whether the number stored in this Size really represents a size (as in a length of something on screen). - * @return false if the unit an angle, a duration, a frequency or the number is a component in a Color object. - */ - public function isSize() { - if (in_array($this->sUnit, explode('/', self::NON_SIZE_UNITS))) { - return false; - } - return !$this->isColorComponent(); - } + /** + * @param float|int|string $fSize + */ + public function setSize($fSize) + { + $this->fSize = (float)$fSize; + } - public function isRelative() { - if (in_array($this->sUnit, explode('/', self::RELATIVE_SIZE_UNITS))) { - return true; - } - if ($this->sUnit === null && $this->fSize != 0) { - return true; - } - return false; - } + /** + * @return float + */ + public function getSize() + { + return $this->fSize; + } - public function __toString() { - return $this->render(new \Sabberworm\CSS\OutputFormat()); - } + /** + * @return bool + */ + public function isColorComponent() + { + return $this->bIsColorComponent; + } - public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) { - $l = localeconv(); - $sPoint = preg_quote($l['decimal_point'], '/'); - return preg_replace(array("/$sPoint/", "/^(-?)0\./"), array('.', '$1.'), $this->fSize) . ($this->sUnit === null ? '' : $this->sUnit); - } + /** + * Returns whether the number stored in this Size really represents a size (as in a length of something on screen). + * + * @return false if the unit an angle, a duration, a frequency or the number is a component in a Color object. + */ + public function isSize() + { + if (in_array($this->sUnit, self::NON_SIZE_UNITS, true)) { + return false; + } + return !$this->isColorComponent(); + } + /** + * @return bool + */ + public function isRelative() + { + if (in_array($this->sUnit, self::RELATIVE_SIZE_UNITS, true)) { + return true; + } + if ($this->sUnit === null && $this->fSize != 0) { + return true; + } + return false; + } + + /** + * @return string + */ + public function __toString() + { + return $this->render(new OutputFormat()); + } + + /** + * @return string + */ + public function render(OutputFormat $oOutputFormat) + { + $l = localeconv(); + $sPoint = preg_quote($l['decimal_point'], '/'); + $sSize = preg_match("/[\d\.]+e[+-]?\d+/i", (string)$this->fSize) + ? preg_replace("/$sPoint?0+$/", "", sprintf("%f", $this->fSize)) : $this->fSize; + return preg_replace(["/$sPoint/", "/^(-?)0\./"], ['.', '$1.'], $sSize) + . ($this->sUnit === null ? '' : $this->sUnit); + } } diff --git a/lib/php-css-parser/Value/URL.php b/lib/php-css-parser/Value/URL.php index b4f37e16416..1467d505c5c 100644 --- a/lib/php-css-parser/Value/URL.php +++ b/lib/php-css-parser/Value/URL.php @@ -2,48 +2,81 @@ namespace Sabberworm\CSS\Value; +use Sabberworm\CSS\OutputFormat; use Sabberworm\CSS\Parsing\ParserState; +use Sabberworm\CSS\Parsing\SourceException; +use Sabberworm\CSS\Parsing\UnexpectedEOFException; +use Sabberworm\CSS\Parsing\UnexpectedTokenException; -class URL extends PrimitiveValue { +class URL extends PrimitiveValue +{ + /** + * @var CSSString + */ + private $oURL; - private $oURL; + /** + * @param int $iLineNo + */ + public function __construct(CSSString $oURL, $iLineNo = 0) + { + parent::__construct($iLineNo); + $this->oURL = $oURL; + } - public function __construct(CSSString $oURL, $iLineNo = 0) { - parent::__construct($iLineNo); - $this->oURL = $oURL; - } + /** + * @return URL + * + * @throws SourceException + * @throws UnexpectedEOFException + * @throws UnexpectedTokenException + */ + public static function parse(ParserState $oParserState) + { + $bUseUrl = $oParserState->comes('url', true); + if ($bUseUrl) { + $oParserState->consume('url'); + $oParserState->consumeWhiteSpace(); + $oParserState->consume('('); + } + $oParserState->consumeWhiteSpace(); + $oResult = new URL(CSSString::parse($oParserState), $oParserState->currentLine()); + if ($bUseUrl) { + $oParserState->consumeWhiteSpace(); + $oParserState->consume(')'); + } + return $oResult; + } - public static function parse(ParserState $oParserState) { - $bUseUrl = $oParserState->comes('url', true); - if ($bUseUrl) { - $oParserState->consume('url'); - $oParserState->consumeWhiteSpace(); - $oParserState->consume('('); - } - $oParserState->consumeWhiteSpace(); - $oResult = new URL(CSSString::parse($oParserState), $oParserState->currentLine()); - if ($bUseUrl) { - $oParserState->consumeWhiteSpace(); - $oParserState->consume(')'); - } - return $oResult; - } + /** + * @return void + */ + public function setURL(CSSString $oURL) + { + $this->oURL = $oURL; + } + /** + * @return CSSString + */ + public function getURL() + { + return $this->oURL; + } - public function setURL(CSSString $oURL) { - $this->oURL = $oURL; - } + /** + * @return string + */ + public function __toString() + { + return $this->render(new OutputFormat()); + } - public function getURL() { - return $this->oURL; - } - - public function __toString() { - return $this->render(new \Sabberworm\CSS\OutputFormat()); - } - - public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) { - return "url({$this->oURL->render($oOutputFormat)})"; - } - -} \ No newline at end of file + /** + * @return string + */ + public function render(OutputFormat $oOutputFormat) + { + return "url({$this->oURL->render($oOutputFormat)})"; + } +} diff --git a/lib/php-css-parser/Value/Value.php b/lib/php-css-parser/Value/Value.php index fccc26bb51c..66cb9fd47ee 100644 --- a/lib/php-css-parser/Value/Value.php +++ b/lib/php-css-parser/Value/Value.php @@ -3,129 +3,196 @@ namespace Sabberworm\CSS\Value; use Sabberworm\CSS\Parsing\ParserState; +use Sabberworm\CSS\Parsing\SourceException; +use Sabberworm\CSS\Parsing\UnexpectedEOFException; use Sabberworm\CSS\Parsing\UnexpectedTokenException; use Sabberworm\CSS\Renderable; -abstract class Value implements Renderable { - protected $iLineNo; +abstract class Value implements Renderable +{ + /** + * @var int + */ + protected $iLineNo; - public function __construct($iLineNo = 0) { - $this->iLineNo = $iLineNo; - } + /** + * @param int $iLineNo + */ + public function __construct($iLineNo = 0) + { + $this->iLineNo = $iLineNo; + } - public static function parseValue(ParserState $oParserState, $aListDelimiters = array()) { - $aStack = array(); - $oParserState->consumeWhiteSpace(); - //Build a list of delimiters and parsed values - while (!($oParserState->comes('}') || $oParserState->comes(';') || $oParserState->comes('!') || $oParserState->comes(')') || $oParserState->comes('\\'))) { - if (count($aStack) > 0) { - $bFoundDelimiter = false; - foreach ($aListDelimiters as $sDelimiter) { - if ($oParserState->comes($sDelimiter)) { - array_push($aStack, $oParserState->consume($sDelimiter)); - $oParserState->consumeWhiteSpace(); - $bFoundDelimiter = true; - break; - } - } - if (!$bFoundDelimiter) { - //Whitespace was the list delimiter - array_push($aStack, ' '); - } - } - array_push($aStack, self::parsePrimitiveValue($oParserState)); - $oParserState->consumeWhiteSpace(); - } - //Convert the list to list objects - foreach ($aListDelimiters as $sDelimiter) { - if (count($aStack) === 1) { - return $aStack[0]; - } - $iStartPosition = null; - while (($iStartPosition = array_search($sDelimiter, $aStack, true)) !== false) { - $iLength = 2; //Number of elements to be joined - for ($i = $iStartPosition + 2; $i < count($aStack); $i+=2, ++$iLength) { - if ($sDelimiter !== $aStack[$i]) { - break; - } - } - $oList = new RuleValueList($sDelimiter, $oParserState->currentLine()); - for ($i = $iStartPosition - 1; $i - $iStartPosition + 1 < $iLength * 2; $i+=2) { - $oList->addListComponent($aStack[$i]); - } - array_splice($aStack, $iStartPosition - 1, $iLength * 2 - 1, array($oList)); - } - } - if (!isset($aStack[0])) { - throw new UnexpectedTokenException(" {$oParserState->peek()} ", $oParserState->peek(1, -1) . $oParserState->peek(2), 'literal', $oParserState->currentLine()); - } - return $aStack[0]; - } + /** + * @param array $aListDelimiters + * + * @return RuleValueList|CSSFunction|CSSString|LineName|Size|URL|string + * + * @throws UnexpectedTokenException + * @throws UnexpectedEOFException + */ + public static function parseValue(ParserState $oParserState, array $aListDelimiters = []) + { + /** @var array $aStack */ + $aStack = []; + $oParserState->consumeWhiteSpace(); + //Build a list of delimiters and parsed values + while ( + !($oParserState->comes('}') || $oParserState->comes(';') || $oParserState->comes('!') + || $oParserState->comes(')') + || $oParserState->comes('\\')) + ) { + if (count($aStack) > 0) { + $bFoundDelimiter = false; + foreach ($aListDelimiters as $sDelimiter) { + if ($oParserState->comes($sDelimiter)) { + array_push($aStack, $oParserState->consume($sDelimiter)); + $oParserState->consumeWhiteSpace(); + $bFoundDelimiter = true; + break; + } + } + if (!$bFoundDelimiter) { + //Whitespace was the list delimiter + array_push($aStack, ' '); + } + } + array_push($aStack, self::parsePrimitiveValue($oParserState)); + $oParserState->consumeWhiteSpace(); + } + // Convert the list to list objects + foreach ($aListDelimiters as $sDelimiter) { + if (count($aStack) === 1) { + return $aStack[0]; + } + $iStartPosition = null; + while (($iStartPosition = array_search($sDelimiter, $aStack, true)) !== false) { + $iLength = 2; //Number of elements to be joined + for ($i = $iStartPosition + 2; $i < count($aStack); $i += 2, ++$iLength) { + if ($sDelimiter !== $aStack[$i]) { + break; + } + } + $oList = new RuleValueList($sDelimiter, $oParserState->currentLine()); + for ($i = $iStartPosition - 1; $i - $iStartPosition + 1 < $iLength * 2; $i += 2) { + $oList->addListComponent($aStack[$i]); + } + array_splice($aStack, $iStartPosition - 1, $iLength * 2 - 1, [$oList]); + } + } + if (!isset($aStack[0])) { + throw new UnexpectedTokenException( + " {$oParserState->peek()} ", + $oParserState->peek(1, -1) . $oParserState->peek(2), + 'literal', + $oParserState->currentLine() + ); + } + return $aStack[0]; + } - public static function parseIdentifierOrFunction(ParserState $oParserState, $bIgnoreCase = false) { - $sResult = $oParserState->parseIdentifier($bIgnoreCase); + /** + * @param bool $bIgnoreCase + * + * @return CSSFunction|string + * + * @throws UnexpectedEOFException + * @throws UnexpectedTokenException + */ + public static function parseIdentifierOrFunction(ParserState $oParserState, $bIgnoreCase = false) + { + $sResult = $oParserState->parseIdentifier($bIgnoreCase); - if ($oParserState->comes('(')) { - $oParserState->consume('('); - $aArguments = Value::parseValue($oParserState, array('=', ' ', ',')); - $sResult = new CSSFunction($sResult, $aArguments, ',', $oParserState->currentLine()); - $oParserState->consume(')'); - } + if ($oParserState->comes('(')) { + $oParserState->consume('('); + $aArguments = Value::parseValue($oParserState, ['=', ' ', ',']); + $sResult = new CSSFunction($sResult, $aArguments, ',', $oParserState->currentLine()); + $oParserState->consume(')'); + } - return $sResult; - } + return $sResult; + } - public static function parsePrimitiveValue(ParserState $oParserState) { - $oValue = null; - $oParserState->consumeWhiteSpace(); - if (is_numeric($oParserState->peek()) || ($oParserState->comes('-.') && is_numeric($oParserState->peek(1, 2))) || (($oParserState->comes('-') || $oParserState->comes('.')) && is_numeric($oParserState->peek(1, 1)))) { - $oValue = Size::parse($oParserState); - } else if ($oParserState->comes('#') || $oParserState->comes('rgb', true) || $oParserState->comes('hsl', true)) { - $oValue = Color::parse($oParserState); - } else if ($oParserState->comes('url', true)) { - $oValue = URL::parse($oParserState); - } else if ($oParserState->comes('calc', true) || $oParserState->comes('-webkit-calc', true) || $oParserState->comes('-moz-calc', true)) { - $oValue = CalcFunction::parse($oParserState); - } else if ($oParserState->comes("'") || $oParserState->comes('"')) { - $oValue = CSSString::parse($oParserState); - } else if ($oParserState->comes("progid:") && $oParserState->getSettings()->bLenientParsing) { - $oValue = self::parseMicrosoftFilter($oParserState); - } else if ($oParserState->comes("[")) { - $oValue = LineName::parse($oParserState); - } else if ($oParserState->comes("U+")) { - $oValue = self::parseUnicodeRangeValue($oParserState); - } else { - $oValue = self::parseIdentifierOrFunction($oParserState); - } - $oParserState->consumeWhiteSpace(); - return $oValue; - } + /** + * @return CSSFunction|CSSString|LineName|Size|URL|string + * + * @throws UnexpectedEOFException + * @throws UnexpectedTokenException + * @throws SourceException + */ + public static function parsePrimitiveValue(ParserState $oParserState) + { + $oValue = null; + $oParserState->consumeWhiteSpace(); + if ( + is_numeric($oParserState->peek()) + || ($oParserState->comes('-.') + && is_numeric($oParserState->peek(1, 2))) + || (($oParserState->comes('-') || $oParserState->comes('.')) && is_numeric($oParserState->peek(1, 1))) + ) { + $oValue = Size::parse($oParserState); + } elseif ($oParserState->comes('#') || $oParserState->comes('rgb', true) || $oParserState->comes('hsl', true)) { + $oValue = Color::parse($oParserState); + } elseif ($oParserState->comes('url', true)) { + $oValue = URL::parse($oParserState); + } elseif ( + $oParserState->comes('calc', true) || $oParserState->comes('-webkit-calc', true) + || $oParserState->comes('-moz-calc', true) + ) { + $oValue = CalcFunction::parse($oParserState); + } elseif ($oParserState->comes("'") || $oParserState->comes('"')) { + $oValue = CSSString::parse($oParserState); + } elseif ($oParserState->comes("progid:") && $oParserState->getSettings()->bLenientParsing) { + $oValue = self::parseMicrosoftFilter($oParserState); + } elseif ($oParserState->comes("[")) { + $oValue = LineName::parse($oParserState); + } elseif ($oParserState->comes("U+")) { + $oValue = self::parseUnicodeRangeValue($oParserState); + } else { + $oValue = self::parseIdentifierOrFunction($oParserState); + } + $oParserState->consumeWhiteSpace(); + return $oValue; + } - private static function parseMicrosoftFilter(ParserState $oParserState) { - $sFunction = $oParserState->consumeUntil('(', false, true); - $aArguments = Value::parseValue($oParserState, array(',', '=')); - return new CSSFunction($sFunction, $aArguments, ',', $oParserState->currentLine()); - } + /** + * @return CSSFunction + * + * @throws UnexpectedEOFException + * @throws UnexpectedTokenException + */ + private static function parseMicrosoftFilter(ParserState $oParserState) + { + $sFunction = $oParserState->consumeUntil('(', false, true); + $aArguments = Value::parseValue($oParserState, [',', '=']); + return new CSSFunction($sFunction, $aArguments, ',', $oParserState->currentLine()); + } - private static function parseUnicodeRangeValue(ParserState $oParserState) { - $iCodepointMaxLenth = 6; // Code points outside BMP can use up to six digits - $sRange = ""; - $oParserState->consume("U+"); - do { - if ($oParserState->comes('-')) $iCodepointMaxLenth = 13; // Max length is 2 six digit code points + the dash(-) between them - $sRange .= $oParserState->consume(1); - } while (strlen($sRange) < $iCodepointMaxLenth && preg_match("/[A-Fa-f0-9\?-]/", $oParserState->peek())); - return "U+{$sRange}"; - } - - /** - * @return int - */ - public function getLineNo() { - return $this->iLineNo; - } + /** + * @return string + * + * @throws UnexpectedEOFException + * @throws UnexpectedTokenException + */ + private static function parseUnicodeRangeValue(ParserState $oParserState) + { + $iCodepointMaxLength = 6; // Code points outside BMP can use up to six digits + $sRange = ""; + $oParserState->consume("U+"); + do { + if ($oParserState->comes('-')) { + $iCodepointMaxLength = 13; // Max length is 2 six digit code points + the dash(-) between them + } + $sRange .= $oParserState->consume(1); + } while (strlen($sRange) < $iCodepointMaxLength && preg_match("/[A-Fa-f0-9\?-]/", $oParserState->peek())); + return "U+{$sRange}"; + } - //Methods are commented out because re-declaring them here is a fatal error in PHP < 5.3.9 - //public abstract function __toString(); - //public abstract function render(\Sabberworm\CSS\OutputFormat $oOutputFormat); + /** + * @return int + */ + public function getLineNo() + { + return $this->iLineNo; + } } diff --git a/lib/php-css-parser/Value/ValueList.php b/lib/php-css-parser/Value/ValueList.php index 5c3d0e4fb0f..af5348b9673 100644 --- a/lib/php-css-parser/Value/ValueList.php +++ b/lib/php-css-parser/Value/ValueList.php @@ -2,46 +2,99 @@ namespace Sabberworm\CSS\Value; -abstract class ValueList extends Value { +use Sabberworm\CSS\OutputFormat; - protected $aComponents; - protected $sSeparator; +abstract class ValueList extends Value +{ + /** + * @var array + */ + protected $aComponents; - public function __construct($aComponents = array(), $sSeparator = ',', $iLineNo = 0) { - parent::__construct($iLineNo); - if (!is_array($aComponents)) { - $aComponents = array($aComponents); - } - $this->aComponents = $aComponents; - $this->sSeparator = $sSeparator; - } + /** + * @var string + */ + protected $sSeparator; - public function addListComponent($mComponent) { - $this->aComponents[] = $mComponent; - } + /** + * phpcs:ignore Generic.Files.LineLength + * @param array|RuleValueList|CSSFunction|CSSString|LineName|Size|URL|string $aComponents + * @param string $sSeparator + * @param int $iLineNo + */ + public function __construct($aComponents = [], $sSeparator = ',', $iLineNo = 0) + { + parent::__construct($iLineNo); + if (!is_array($aComponents)) { + $aComponents = [$aComponents]; + } + $this->aComponents = $aComponents; + $this->sSeparator = $sSeparator; + } - public function getListComponents() { - return $this->aComponents; - } + /** + * @param RuleValueList|CSSFunction|CSSString|LineName|Size|URL|string $mComponent + * + * @return void + */ + public function addListComponent($mComponent) + { + $this->aComponents[] = $mComponent; + } - public function setListComponents($aComponents) { - $this->aComponents = $aComponents; - } + /** + * @return array + */ + public function getListComponents() + { + return $this->aComponents; + } - public function getListSeparator() { - return $this->sSeparator; - } + /** + * @param array $aComponents + * + * @return void + */ + public function setListComponents(array $aComponents) + { + $this->aComponents = $aComponents; + } - public function setListSeparator($sSeparator) { - $this->sSeparator = $sSeparator; - } + /** + * @return string + */ + public function getListSeparator() + { + return $this->sSeparator; + } - public function __toString() { - return $this->render(new \Sabberworm\CSS\OutputFormat()); - } + /** + * @param string $sSeparator + * + * @return void + */ + public function setListSeparator($sSeparator) + { + $this->sSeparator = $sSeparator; + } - public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) { - return $oOutputFormat->implode($oOutputFormat->spaceBeforeListArgumentSeparator($this->sSeparator) . $this->sSeparator . $oOutputFormat->spaceAfterListArgumentSeparator($this->sSeparator), $this->aComponents); - } + /** + * @return string + */ + public function __toString() + { + return $this->render(new OutputFormat()); + } + /** + * @return string + */ + public function render(OutputFormat $oOutputFormat) + { + return $oOutputFormat->implode( + $oOutputFormat->spaceBeforeListArgumentSeparator($this->sSeparator) . $this->sSeparator + . $oOutputFormat->spaceAfterListArgumentSeparator($this->sSeparator), + $this->aComponents + ); + } } diff --git a/lib/thirdpartylibs.xml b/lib/thirdpartylibs.xml index 2f87c6aa2ef..adc39fd29fd 100644 --- a/lib/thirdpartylibs.xml +++ b/lib/thirdpartylibs.xml @@ -343,7 +343,7 @@ All rights reserved. php-css-parser PHP-CSS-Parser A Parser for CSS Files written in PHP. - 8.3.1 + 8.4.0 MIT https://github.com/sabberworm/PHP-CSS-Parser From 650694f18fdd8f6f203f45df32572e51a70d1cb7 Mon Sep 17 00:00:00 2001 From: David Woloszyn Date: Thu, 20 Oct 2022 09:31:34 +1100 Subject: [PATCH 2/2] MDL-75475 lib: Applied PHP CSS Parser patches --- lib/php-css-parser/CSSList/CSSList.php | 15 ++++++++++++++- lib/php-css-parser/Rule/Rule.php | 1 - 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/lib/php-css-parser/CSSList/CSSList.php b/lib/php-css-parser/CSSList/CSSList.php index 946740a4999..e24caeeb6f3 100644 --- a/lib/php-css-parser/CSSList/CSSList.php +++ b/lib/php-css-parser/CSSList/CSSList.php @@ -89,7 +89,6 @@ abstract class CSSList implements Renderable, Commentable $oListItem->setComments($comments); $oList->append($oListItem); } - $oParserState->consumeWhiteSpace(); } if (!$bIsRoot && !$bLenientParsing) { throw new SourceException("Unexpected end of document", $oParserState->currentLine()); @@ -283,6 +282,20 @@ abstract class CSSList implements Renderable, Commentable $this->aContents[] = $oItem; } + /** + * Insert an item before its sibling. + * + * @param mixed $oItem The item. + * @param mixed $oSibling The sibling. + */ + public function insert($oItem, $oSibling) { + $iIndex = array_search($oSibling, $this->aContents); + if ($iIndex === false) { + return $this->append($oItem); + } + array_splice($this->aContents, $iIndex, 0, array($oItem)); + } + /** * Splices the list of contents. * diff --git a/lib/php-css-parser/Rule/Rule.php b/lib/php-css-parser/Rule/Rule.php index c1ea6df74e5..52b97f38e15 100644 --- a/lib/php-css-parser/Rule/Rule.php +++ b/lib/php-css-parser/Rule/Rule.php @@ -106,7 +106,6 @@ class Rule implements Renderable, Commentable while ($oParserState->comes(';')) { $oParserState->consume(';'); } - $oParserState->consumeWhiteSpace(); return $oRule; }