diff --git a/lib/php-css-parser/CSSList/AtRuleBlockList.php b/lib/php-css-parser/CSSList/AtRuleBlockList.php new file mode 100644 index 00000000000..a1ff8f8d10e --- /dev/null +++ b/lib/php-css-parser/CSSList/AtRuleBlockList.php @@ -0,0 +1,48 @@ +sType = $sType; + $this->sArgs = $sArgs; + } + + public function atRuleName() { + return $this->sType; + } + + public function atRuleArgs() { + return $this->sArgs; + } + + public function __toString() { + return $this->render(new \Sabberworm\CSS\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; + } + + public function isRootList() { + return false; + } + +} \ No newline at end of file diff --git a/lib/php-css-parser/CSSList/CSSBlockList.php b/lib/php-css-parser/CSSList/CSSBlockList.php new file mode 100644 index 00000000000..17c68142eb7 --- /dev/null +++ b/lib/php-css-parser/CSSList/CSSBlockList.php @@ -0,0 +1,82 @@ +aContents as $mContent) { + if ($mContent instanceof DeclarationBlock) { + $aResult[] = $mContent; + } else if ($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); + } + } + } + + 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 { + $sComparison = "\$bRes = {$oSelector->getSpecificity()} $sSpecificitySearch;"; + eval($sComparison); + if ($bRes) { + $aResult[] = $oSelector; + } + } + } + } + } + +} diff --git a/lib/php-css-parser/CSSList/CSSList.php b/lib/php-css-parser/CSSList/CSSList.php new file mode 100644 index 00000000000..bc90460b925 --- /dev/null +++ b/lib/php-css-parser/CSSList/CSSList.php @@ -0,0 +1,157 @@ +aComments = array(); + $this->aContents = array(); + $this->iLineNo = $iLineNo; + } + + /** + * @return int + */ + public function getLineNo() { + return $this->iLineNo; + } + + public function append($oItem) { + $this->aContents[] = $oItem; + } + + /** + * 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) + */ + public function remove($oItemToRemove) { + $iKey = array_search($oItemToRemove, $this->aContents, true); + if ($iKey !== false) { + unset($this->aContents[$iKey]); + return true; + } + return false; + } + + /** + * 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); + } + } + + /** + * 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; + } + } + } + } + + public function __toString() { + return $this->render(new \Sabberworm\CSS\OutputFormat()); + } + + 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; + } + + if(!$bIsFirst) { + // Had some output + $sResult .= $oOutputFormat->spaceAfterBlocks(); + } + + return $sResult; + } + + /** + * Return true if the list can not be further outdented. Only important when rendering. + */ + public abstract function isRootList(); + + public function getContents() { + return $this->aContents; + } + + /** + * @param array $aComments Array of comments. + */ + public function addComments(array $aComments) { + $this->aComments = array_merge($this->aComments, $aComments); + } + + /** + * @return array + */ + public function getComments() { + return $this->aComments; + } + + /** + * @param array $aComments Array containing Comment objects. + */ + 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 new file mode 100644 index 00000000000..bd4a23ee319 --- /dev/null +++ b/lib/php-css-parser/CSSList/Document.php @@ -0,0 +1,105 @@ +allDeclarationBlocks($aResult); + return $aResult; + } + + /** + * @deprecated use getAllDeclarationBlocks() + */ + 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 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 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) { + if (is_numeric($sSpecificitySearch) || is_numeric($sSpecificitySearch[0])) { + $sSpecificitySearch = "== $sSpecificitySearch"; + } + $aResult = array(); + $this->allSelectors($aResult, $sSpecificitySearch); + return $aResult; + } + + /** + * Expands all shorthand properties to their long value + */ + 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(); + } + } + + // 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); + } + + public function isRootList() { + return true; + } + +} \ No newline at end of file diff --git a/lib/php-css-parser/CSSList/KeyFrame.php b/lib/php-css-parser/CSSList/KeyFrame.php new file mode 100644 index 00000000000..0334b1b3f18 --- /dev/null +++ b/lib/php-css-parser/CSSList/KeyFrame.php @@ -0,0 +1,56 @@ +vendorKeyFrame = null; + $this->animationName = null; + } + + public function setVendorKeyFrame($vendorKeyFrame) { + $this->vendorKeyFrame = $vendorKeyFrame; + } + + public function getVendorKeyFrame() { + return $this->vendorKeyFrame; + } + + public function setAnimationName($animationName) { + $this->animationName = $animationName; + } + + public function getAnimationName() { + return $this->animationName; + } + + public function __toString() { + return $this->render(new \Sabberworm\CSS\OutputFormat()); + } + + public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) { + $sResult = "@{$this->vendorKeyFrame} {$this->animationName}{$oOutputFormat->spaceBeforeOpeningBrace()}{"; + $sResult .= parent::render($oOutputFormat); + $sResult .= '}'; + return $sResult; + } + + public function isRootList() { + return false; + } + + public function atRuleName() { + return $this->vendorKeyFrame; + } + + public function atRuleArgs() { + return $this->animationName; + } +} diff --git a/lib/php-css-parser/Comment/Comment.php b/lib/php-css-parser/Comment/Comment.php new file mode 100644 index 00000000000..70521b16a3d --- /dev/null +++ b/lib/php-css-parser/Comment/Comment.php @@ -0,0 +1,51 @@ +sComment = $sComment; + $this->iLineNo = $iLineNo; + } + + /** + * @return string + */ + public function getComment() { + return $this->sComment; + } + + /** + * @return int + */ + public function getLineNo() { + return $this->iLineNo; + } + + /** + * @return string + */ + public function setComment($sComment) { + $this->sComment = $sComment; + } + + /** + * @return string + */ + public function __toString() { + return $this->render(new \Sabberworm\CSS\OutputFormat()); + } + + /** + * @return string + */ + public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) { + return '/*' . $this->sComment . '*/'; + } + +} diff --git a/lib/php-css-parser/Comment/Commentable.php b/lib/php-css-parser/Comment/Commentable.php new file mode 100644 index 00000000000..3100f17a8fa --- /dev/null +++ b/lib/php-css-parser/Comment/Commentable.php @@ -0,0 +1,23 @@ +set('Space*Rules', "\n");`) + */ + public $sSpaceAfterRuleName = ' '; + + public $sSpaceBeforeRules = ''; + public $sSpaceAfterRules = ''; + public $sSpaceBetweenRules = ''; + + public $sSpaceBeforeBlocks = ''; + public $sSpaceAfterBlocks = ''; + public $sSpaceBetweenBlocks = "\n"; + + // 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 = ' '; + + /** + * 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; + } + + public static function create() { + return new OutputFormat(); + } + + public static function createCompact() { + return self::create()->set('Space*Rules', "")->set('Space*Blocks', "")->setSpaceAfterRuleName('')->setSpaceBeforeOpeningBrace('')->setSpaceAfterSelectorSeparator(''); + } + + public static function createPretty() { + return self::create()->set('Space*Rules', "\n")->set('Space*Blocks', "\n")->setSpaceBetweenBlocks("\n\n")->set('SpaceAfterListArgumentSeparator', array('default' => '', ',' => ' ')); + } +} + +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()); + } +} \ No newline at end of file diff --git a/lib/php-css-parser/Parser.php b/lib/php-css-parser/Parser.php new file mode 100644 index 00000000000..409b796d618 --- /dev/null +++ b/lib/php-css-parser/Parser.php @@ -0,0 +1,727 @@ +sText = $sText; + $this->iCurrentPosition = 0; + $this->iLineNo = $iLineNo; + if ($oParserSettings === null) { + $oParserSettings = Settings::create(); + } + $this->oParserSettings = $oParserSettings; + $this->blockRules = explode('/', AtRule::BLOCK_RULES); + + foreach (explode('/', Size::ABSOLUTE_SIZE_UNITS.'/'.Size::RELATIVE_SIZE_UNITS.'/'.Size::NON_SIZE_UNITS) as $val) { + $iSize = strlen($val); + if(!isset($this->aSizeUnits[$iSize])) { + $this->aSizeUnits[$iSize] = array(); + } + $this->aSizeUnits[$iSize][strtolower($val)] = $val; + } + ksort($this->aSizeUnits, SORT_NUMERIC); + } + + public function setCharset($sCharset) { + $this->sCharset = $sCharset; + $this->aText = $this->strsplit($this->sText); + $this->iLength = count($this->aText); + } + + public function getCharset() { + return $this->sCharset; + } + + public function parse() { + $this->setCharset($this->oParserSettings->sDefaultCharset); + $oResult = new Document($this->iLineNo); + $this->parseDocument($oResult); + return $oResult; + } + + private function parseDocument(Document $oDocument) { + $this->parseList($oDocument, true); + } + + private function parseList(CSSList $oList, $bIsRoot = false) { + while (!$this->isEnd()) { + $comments = $this->consumeWhiteSpace(); + $oListItem = null; + if($this->oParserSettings->bLenientParsing) { + try { + $oListItem = $this->parseListItem($oList, $bIsRoot); + } catch (UnexpectedTokenException $e) { + $oListItem = false; + } + } else { + $oListItem = $this->parseListItem($oList, $bIsRoot); + } + if($oListItem === null) { + // List parsing finished + return; + } + if($oListItem) { + $oListItem->setComments($comments); + $oList->append($oListItem); + } + } + if (!$bIsRoot) { + throw new SourceException("Unexpected end of document", $this->iLineNo); + } + } + + private function parseListItem(CSSList $oList, $bIsRoot = false) { + if ($this->comes('@')) { + $oAtRule = $this->parseAtRule(); + if($oAtRule instanceof Charset) { + if(!$bIsRoot) { + throw new UnexpectedTokenException('@charset may only occur in root document', '', 'custom', $this->iLineNo); + } + if(count($oList->getContents()) > 0) { + throw new UnexpectedTokenException('@charset must be the first parseable token in a document', '', 'custom', $this->iLineNo); + } + $this->setCharset($oAtRule->getCharset()->getString()); + } + return $oAtRule; + } else if ($this->comes('}')) { + $this->consume('}'); + if ($bIsRoot) { + throw new SourceException("Unopened {", $this->iLineNo); + } else { + return null; + } + } else { + return $this->parseSelector(); + } + } + + private function parseAtRule() { + $this->consume('@'); + $sIdentifier = $this->parseIdentifier(false); + $iIdentifierLineNum = $this->iLineNo; + $this->consumeWhiteSpace(); + if ($sIdentifier === 'import') { + $oLocation = $this->parseURLValue(); + $this->consumeWhiteSpace(); + $sMediaQuery = null; + if (!$this->comes(';')) { + $sMediaQuery = $this->consumeUntil(';'); + } + $this->consume(';'); + return new Import($oLocation, $sMediaQuery, $iIdentifierLineNum); + } else if ($sIdentifier === 'charset') { + $sCharset = $this->parseStringValue(); + $this->consumeWhiteSpace(); + $this->consume(';'); + return new Charset($sCharset, $iIdentifierLineNum); + } else if ($this->identifierIs($sIdentifier, 'keyframes')) { + $oResult = new KeyFrame($iIdentifierLineNum); + $oResult->setVendorKeyFrame($sIdentifier); + $oResult->setAnimationName(trim($this->consumeUntil('{', false, true))); + $this->parseList($oResult); + return $oResult; + } else if ($sIdentifier === 'namespace') { + $sPrefix = null; + $mUrl = $this->parsePrimitiveValue(); + if (!$this->comes(';')) { + $sPrefix = $mUrl; + $mUrl = $this->parsePrimitiveValue(); + } + $this->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($this->consumeUntil('{', false, true)); + $bUseRuleSet = true; + foreach($this->blockRules as $sBlockRuleName) { + if($this->identifierIs($sIdentifier, $sBlockRuleName)) { + $bUseRuleSet = false; + break; + } + } + if($bUseRuleSet) { + $oAtRule = new AtRuleSet($sIdentifier, $sArgs, $iIdentifierLineNum); + $this->parseRuleSet($oAtRule); + } else { + $oAtRule = new AtRuleBlockList($sIdentifier, $sArgs, $iIdentifierLineNum); + $this->parseList($oAtRule); + } + return $oAtRule; + } + } + + private function parseIdentifier($bAllowFunctions = true, $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); + } + if ($bAllowFunctions && $this->comes('(')) { + $this->consume('('); + $aArguments = $this->parseValue(array('=', ' ', ',')); + $sResult = new CSSFunction($sResult, $aArguments, ',', $this->iLineNo); + $this->consume(')'); + } + return $sResult; + } + + private function parseStringValue() { + $sBegin = $this->peek(); + $sQuote = null; + if ($sBegin === "'") { + $sQuote = "'"; + } else if ($sBegin === '"') { + $sQuote = '"'; + } + if ($sQuote !== null) { + $this->consume($sQuote); + } + $sResult = ""; + $sContent = null; + if ($sQuote === null) { + //Unquoted strings end in whitespace or with braces, brackets, parentheses + while (!preg_match('/[\\s{}()<>\\[\\]]/isu', $this->peek())) { + $sResult .= $this->parseCharacter(false); + } + } else { + while (!$this->comes($sQuote)) { + $sContent = $this->parseCharacter(false); + if ($sContent === null) { + throw new SourceException("Non-well-formed quoted string {$this->peek(3)}", $this->iLineNo); + } + $sResult .= $sContent; + } + $this->consume($sQuote); + } + return new CSSString($sResult, $this->iLineNo); + } + + private 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'); + 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; + } + + private function parseSelector() { + $aComments = array(); + $oResult = new DeclarationBlock($this->iLineNo); + $oResult->setSelector($this->consumeUntil('{', false, true, $aComments)); + $oResult->setComments($aComments); + $this->parseRuleSet($oResult); + return $oResult; + } + + private function parseRuleSet($oRuleSet) { + while ($this->comes(';')) { + $this->consume(';'); + } + while (!$this->comes('}')) { + $oRule = null; + if($this->oParserSettings->bLenientParsing) { + try { + $oRule = $this->parseRule(); + } catch (UnexpectedTokenException $e) { + try { + $sConsume = $this->consumeUntil(array("\n", ";", '}'), true); + // We need to “unfind” the matches to the end of the ruleSet as this will be matched later + if($this->streql(substr($sConsume, -1), '}')) { + --$this->iCurrentPosition; + } else { + while ($this->comes(';')) { + $this->consume(';'); + } + } + } catch (UnexpectedTokenException $e) { + // We’ve reached the end of the document. Just close the RuleSet. + return; + } + } + } else { + $oRule = $this->parseRule(); + } + if($oRule) { + $oRuleSet->addRule($oRule); + } + } + $this->consume('}'); + } + + private function parseRule() { + $aComments = $this->consumeWhiteSpace(); + $oRule = new Rule($this->parseIdentifier(), $this->iLineNo); + $oRule->setComments($aComments); + $oRule->addComments($this->consumeWhiteSpace()); + $this->consume(':'); + $oValue = $this->parseValue(self::listDelimiterForRule($oRule->getRule())); + $oRule->setValue($oValue); + if ($this->oParserSettings->bLenientParsing) { + while ($this->comes('\\')) { + $this->consume('\\'); + $oRule->addIeHack($this->consume()); + $this->consumeWhiteSpace(); + } + } + if ($this->comes('!')) { + $this->consume('!'); + $this->consumeWhiteSpace(); + $this->consume('important'); + $oRule->setIsImportant(true); + } + while ($this->comes(';')) { + $this->consume(';'); + } + return $oRule; + } + + private function parseValue($aListDelimiters) { + $aStack = array(); + $this->consumeWhiteSpace(); + //Build a list of delimiters and parsed values + while (!($this->comes('}') || $this->comes(';') || $this->comes('!') || $this->comes(')') || $this->comes('\\'))) { + if (count($aStack) > 0) { + $bFoundDelimiter = false; + foreach ($aListDelimiters as $sDelimiter) { + if ($this->comes($sDelimiter)) { + array_push($aStack, $this->consume($sDelimiter)); + $this->consumeWhiteSpace(); + $bFoundDelimiter = true; + break; + } + } + if (!$bFoundDelimiter) { + //Whitespace was the list delimiter + array_push($aStack, ' '); + } + } + array_push($aStack, $this->parsePrimitiveValue()); + $this->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, $this->iLineNo); + 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)); + } + } + return $aStack[0]; + } + + private static function listDelimiterForRule($sRule) { + if (preg_match('/^font($|-)/', $sRule)) { + return array(',', '/', ' '); + } + return array(',', ' ', '/'); + } + + private function parsePrimitiveValue() { + $oValue = null; + $this->consumeWhiteSpace(); + if (is_numeric($this->peek()) || ($this->comes('-.') && is_numeric($this->peek(1, 2))) || (($this->comes('-') || $this->comes('.')) && is_numeric($this->peek(1, 1)))) { + $oValue = $this->parseNumericValue(); + } else if ($this->comes('#') || $this->comes('rgb', true) || $this->comes('hsl', true)) { + $oValue = $this->parseColorValue(); + } else if ($this->comes('url', true)) { + $oValue = $this->parseURLValue(); + } else if ($this->comes("'") || $this->comes('"')) { + $oValue = $this->parseStringValue(); + } else if ($this->comes("progid:") && $this->oParserSettings->bLenientParsing) { + $oValue = $this->parseMicrosoftFilter(); + } else { + $oValue = $this->parseIdentifier(true, false); + } + $this->consumeWhiteSpace(); + return $oValue; + } + + private function parseNumericValue($bForColor = false) { + $sSize = ''; + if ($this->comes('-')) { + $sSize .= $this->consume('-'); + } + while (is_numeric($this->peek()) || $this->comes('.')) { + if ($this->comes('.')) { + $sSize .= $this->consume('.'); + } else { + $sSize .= $this->consume(1); + } + } + + $sUnit = null; + foreach ($this->aSizeUnits as $iLength => &$aValues) { + $sKey = strtolower($this->peek($iLength)); + if(array_key_exists($sKey, $aValues)) { + if (($sUnit = $aValues[$sKey]) !== null) { + $this->consume($iLength); + break; + } + } + } + return new Size(floatval($sSize), $sUnit, $bForColor, $this->iLineNo); + } + + private function parseColorValue() { + $aColor = array(); + if ($this->comes('#')) { + $this->consume('#'); + $sValue = $this->parseIdentifier(false); + if ($this->strlen($sValue) === 3) { + $sValue = $sValue[0] . $sValue[0] . $sValue[1] . $sValue[1] . $sValue[2] . $sValue[2]; + } + $aColor = array('r' => new Size(intval($sValue[0] . $sValue[1], 16), null, true, $this->iLineNo), 'g' => new Size(intval($sValue[2] . $sValue[3], 16), null, true, $this->iLineNo), 'b' => new Size(intval($sValue[4] . $sValue[5], 16), null, true, $this->iLineNo)); + } else { + $sColorMode = $this->parseIdentifier(false); + $this->consumeWhiteSpace(); + $this->consume('('); + $iLength = $this->strlen($sColorMode); + for ($i = 0; $i < $iLength; ++$i) { + $this->consumeWhiteSpace(); + $aColor[$sColorMode[$i]] = $this->parseNumericValue(true); + $this->consumeWhiteSpace(); + if ($i < ($iLength - 1)) { + $this->consume(','); + } + } + $this->consume(')'); + } + return new Color($aColor, $this->iLineNo); + } + + private function parseMicrosoftFilter() { + $sFunction = $this->consumeUntil('(', false, true); + $aArguments = $this->parseValue(array(',', '=')); + return new CSSFunction($sFunction, $aArguments, ',', $this->iLineNo); + } + + private function parseURLValue() { + $bUseUrl = $this->comes('url', true); + if ($bUseUrl) { + $this->consume('url'); + $this->consumeWhiteSpace(); + $this->consume('('); + } + $this->consumeWhiteSpace(); + $oResult = new URL($this->parseStringValue(), $this->iLineNo); + if ($bUseUrl) { + $this->consumeWhiteSpace(); + $this->consume(')'); + } + return $oResult; + } + + /** + * 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 function identifierIs($sIdentifier, $sMatch) { + return (strcasecmp($sIdentifier, $sMatch) === 0) + ?: preg_match("/^(-\\w+-)?$sMatch$/i", $sIdentifier) === 1; + } + + private function comes($sString, $bCaseInsensitive = false) { + $sPeek = $this->peek(strlen($sString)); + return ($sPeek == '') + ? false + : $this->streql($sPeek, $sString, $bCaseInsensitive); + } + + private function peek($iLength = 1, $iOffset = 0) { + $iOffset += $this->iCurrentPosition; + if ($iOffset >= $this->iLength) { + return ''; + } + return $this->substr($iOffset, $iLength); + } + + private 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; + } + } + + private function consumeExpression($mExpression) { + $aMatches = null; + if (preg_match($mExpression, $this->inputLeft(), $aMatches, PREG_OFFSET_CAPTURE) === 1) { + return $this->consume($aMatches[0][0]); + } + throw new UnexpectedTokenException($mExpression, $this->peek(5), 'expression', $this->iLineNo); + } + + private 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 false|Comment + */ + private function consumeComment() { + $mComment = false; + if ($this->comes('/*')) { + $iLineNo = $this->iLineNo; + $this->consume(1); + $mComment = ''; + while (($char = $this->consume(1)) !== '') { + $mComment .= $char; + if ($this->comes('*/')) { + $this->consume(2); + break; + } + } + } + + if ($mComment !== false) { + // We skip the * which was included in the comment. + return new Comment(substr($mComment, 1), $iLineNo); + } + + return $mComment; + } + + private function isEnd() { + return $this->iCurrentPosition >= $this->iLength; + } + + private function consumeUntil($aEnd, $bIncludeEnd = false, $consumeEnd = false, array &$comments = array()) { + $aEnd = is_array($aEnd) ? $aEnd : array($aEnd); + $out = ''; + $start = $this->iCurrentPosition; + + 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; + } + } + + $this->iCurrentPosition = $start; + throw new UnexpectedTokenException('One of ("'.implode('","', $aEnd).'")', $this->peek(5), 'search', $this->iLineNo); + } + + private function inputLeft() { + return $this->substr($this->iCurrentPosition, -1); + } + + 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; + } + + private function strlen($sString) { + if ($this->oParserSettings->bMultibyteSupport) { + return mb_strlen($sString, $this->sCharset); + } else { + return strlen($sString); + } + } + + private function streql($sString1, $sString2, $bCaseInsensitive = true) { + if($bCaseInsensitive) { + return $this->strtolower($sString1) === $this->strtolower($sString2); + } else { + return $sString1 === $sString2; + } + } + + private function strtolower($sString) { + if ($this->oParserSettings->bMultibyteSupport) { + return mb_strtolower($sString, $this->sCharset); + } else { + return strtolower($sString); + } + } + + 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); + } + } + } + + 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/OutputException.php b/lib/php-css-parser/Parsing/OutputException.php new file mode 100644 index 00000000000..1c811770430 --- /dev/null +++ b/lib/php-css-parser/Parsing/OutputException.php @@ -0,0 +1,12 @@ +iLineNo = $iLineNo; + if (!empty($iLineNo)) { + $sMessage .= " [line no: $iLineNo]"; + } + parent::__construct($sMessage); + } + + public function getLineNo() { + return $this->iLineNo; + } +} \ No newline at end of file diff --git a/lib/php-css-parser/Parsing/UnexpectedTokenException.php b/lib/php-css-parser/Parsing/UnexpectedTokenException.php new file mode 100644 index 00000000000..0ef881846a4 --- /dev/null +++ b/lib/php-css-parser/Parsing/UnexpectedTokenException.php @@ -0,0 +1,31 @@ +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"); + } + + parent::__construct($sMessage, $iLineNo); + } +} \ No newline at end of file diff --git a/lib/php-css-parser/Property/AtRule.php b/lib/php-css-parser/Property/AtRule.php new file mode 100644 index 00000000000..de3eea19259 --- /dev/null +++ b/lib/php-css-parser/Property/AtRule.php @@ -0,0 +1,15 @@ +mUrl = $mUrl; + $this->sPrefix = $sPrefix; + $this->iLineNo = $iLineNo; + $this->aComments = array(); + } + + /** + * @return int + */ + public function getLineNo() { + return $this->iLineNo; + } + + public function __toString() { + return $this->render(new \Sabberworm\CSS\OutputFormat()); + } + + public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) { + return '@namespace '.($this->sPrefix === null ? '' : $this->sPrefix.' ').$this->mUrl->render($oOutputFormat).';'; + } + + public function getUrl() { + return $this->mUrl; + } + + public function getPrefix() { + return $this->sPrefix; + } + + public function setUrl($mUrl) { + $this->mUrl = $mUrl; + } + + public function setPrefix($sPrefix) { + $this->sPrefix = $sPrefix; + } + + public function atRuleName() { + return 'namespace'; + } + + public function atRuleArgs() { + $aResult = array($this->mUrl); + if($this->sPrefix) { + array_unshift($aResult, $this->sPrefix); + } + return $aResult; + } + + public function addComments(array $aComments) { + $this->aComments = array_merge($this->aComments, $aComments); + } + + public function getComments() { + return $this->aComments; + } + + public function setComments(array $aComments) { + $this->aComments = $aComments; + } +} \ No newline at end of file diff --git a/lib/php-css-parser/Property/Charset.php b/lib/php-css-parser/Property/Charset.php new file mode 100644 index 00000000000..61c6ebc5b63 --- /dev/null +++ b/lib/php-css-parser/Property/Charset.php @@ -0,0 +1,66 @@ +sCharset = $sCharset; + $this->iLineNo = $iLineNo; + $this->aComments = array(); + } + + /** + * @return int + */ + public function getLineNo() { + return $this->iLineNo; + } + + public function setCharset($sCharset) { + $this->sCharset = $sCharset; + } + + public function getCharset() { + return $this->sCharset; + } + + public function __toString() { + return $this->render(new \Sabberworm\CSS\OutputFormat()); + } + + public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) { + return "@charset {$this->sCharset->render($oOutputFormat)};"; + } + + public function atRuleName() { + return 'charset'; + } + + public function atRuleArgs() { + return $this->sCharset; + } + + public function addComments(array $aComments) { + $this->aComments = array_merge($this->aComments, $aComments); + } + + public function getComments() { + return $this->aComments; + } + + public function setComments(array $aComments) { + $this->aComments = $aComments; + } +} \ No newline at end of file diff --git a/lib/php-css-parser/Property/Import.php b/lib/php-css-parser/Property/Import.php new file mode 100644 index 00000000000..4d40f5a04ca --- /dev/null +++ b/lib/php-css-parser/Property/Import.php @@ -0,0 +1,69 @@ +oLocation = $oLocation; + $this->sMediaQuery = $sMediaQuery; + $this->iLineNo = $iLineNo; + $this->aComments = array(); + } + + /** + * @return int + */ + public function getLineNo() { + return $this->iLineNo; + } + + public function setLocation($oLocation) { + $this->oLocation = $oLocation; + } + + public function getLocation() { + return $this->oLocation; + } + + public function __toString() { + return $this->render(new \Sabberworm\CSS\OutputFormat()); + } + + public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) { + return "@import ".$this->oLocation->render($oOutputFormat).($this->sMediaQuery === null ? '' : ' '.$this->sMediaQuery).';'; + } + + public function atRuleName() { + return 'import'; + } + + public function atRuleArgs() { + $aResult = array($this->oLocation); + if($this->sMediaQuery) { + array_push($aResult, $this->sMediaQuery); + } + return $aResult; + } + + public function addComments(array $aComments) { + $this->aComments = array_merge($this->aComments, $aComments); + } + + public function getComments() { + return $this->aComments; + } + + public function setComments(array $aComments) { + $this->aComments = $aComments; + } +} \ No newline at end of file diff --git a/lib/php-css-parser/Property/Selector.php b/lib/php-css-parser/Property/Selector.php new file mode 100644 index 00000000000..d84171f5ef1 --- /dev/null +++ b/lib/php-css-parser/Property/Selector.php @@ -0,0 +1,74 @@ +\~]+)[\w]+ # elements + | + \:{1,2}( # pseudo-elements + after|before|first-letter|first-line|selection + )) + /ix'; + + private $sSelector; + private $iSpecificity; + + public function __construct($sSelector, $bCalculateSpecificity = false) { + $this->setSelector($sSelector); + if ($bCalculateSpecificity) { + $this->getSpecificity(); + } + } + + public function getSelector() { + return $this->sSelector; + } + + public function setSelector($sSelector) { + $this->sSelector = trim($sSelector); + $this->iSpecificity = null; + } + + public function __toString() { + return $this->getSelector(); + } + + 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 new file mode 100644 index 00000000000..3ac06652e51 --- /dev/null +++ b/lib/php-css-parser/Renderable.php @@ -0,0 +1,9 @@ +sRule = $sRule; + $this->mValue = null; + $this->bIsImportant = false; + $this->aIeHack = array(); + $this->iLineNo = $iLineNo; + $this->aComments = array(); + } + + /** + * @return int + */ + public function getLineNo() { + return $this->iLineNo; + } + + public function setRule($sRule) { + $this->sRule = $sRule; + } + + public function getRule() { + return $this->sRule; + } + + public function getValue() { + return $this->mValue; + } + + public function setValue($mValue) { + $this->mValue = $mValue; + } + + /** + * @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; + } + + /** + * @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; + } + + /** + * 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); + } + } + + public function addIeHack($iModifier) { + $this->aIeHack[] = $iModifier; + } + + public function setIeHack(array $aModifiers) { + $this->aIeHack = $aModifiers; + } + + public function getIeHack() { + return $this->aIeHack; + } + + public function setIsImportant($bIsImportant) { + $this->bIsImportant = $bIsImportant; + } + + public function getIsImportant() { + return $this->bIsImportant; + } + + public function __toString() { + return $this->render(new \Sabberworm\CSS\OutputFormat()); + } + + 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; + } + + /** + * @param array $aComments Array of comments. + */ + public function addComments(array $aComments) { + $this->aComments = array_merge($this->aComments, $aComments); + } + + /** + * @return array + */ + public function getComments() { + return $this->aComments; + } + + /** + * @param array $aComments Array containing Comment objects. + */ + 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 new file mode 100644 index 00000000000..a1042a95aec --- /dev/null +++ b/lib/php-css-parser/RuleSet/AtRuleSet.php @@ -0,0 +1,44 @@ +sType = $sType; + $this->sArgs = $sArgs; + } + + public function atRuleName() { + return $this->sType; + } + + public function atRuleArgs() { + return $this->sArgs; + } + + public function __toString() { + return $this->render(new \Sabberworm\CSS\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 diff --git a/lib/php-css-parser/RuleSet/DeclarationBlock.php b/lib/php-css-parser/RuleSet/DeclarationBlock.php new file mode 100644 index 00000000000..e18f5d82984 --- /dev/null +++ b/lib/php-css-parser/RuleSet/DeclarationBlock.php @@ -0,0 +1,608 @@ +aSelectors = array(); + } + + 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); + } + } + } + + // 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; + } + + /** + * @deprecated use getSelectors() + */ + public function getSelector() { + return $this->getSelectors(); + } + + /** + * @deprecated use setSelectors() + */ + public function setSelector($mSelector) { + $this->setSelectors($mSelector); + } + + public function getSelectors() { + return $this->aSelectors; + } + + /** + * 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(); + } + + /** + * 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(); + } + + /** + * 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); + } + } + + /** + * 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); + } + } + + /** + * 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'); + } + + /* + * 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 + * */ + + 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'); + } + + 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'); + } + + 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); + } + } + + public function createBackgroundShorthand() { + $aProperties = array( + 'background-color', 'background-image', 'background-repeat', + 'background-position', 'background-attachment' + ); + $this->createShorthandProperties($aProperties, 'background'); + } + + public function createListStyleShorthand() { + $aProperties = array( + 'list-style-type', 'list-style-position', 'list-style-image' + ); + $this->createShorthandProperties($aProperties, 'list-style'); + } + + /** + * 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'); + } + + /* + * 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->implode($oOutputFormat->spaceBeforeSelectorSeparator() . ',' . $oOutputFormat->spaceAfterSelectorSeparator(), $this->aSelectors) . $oOutputFormat->spaceBeforeOpeningBrace() . '{'; + $sResult .= parent::render($oOutputFormat); + $sResult .= '}'; + return $sResult; + } + +} diff --git a/lib/php-css-parser/RuleSet/RuleSet.php b/lib/php-css-parser/RuleSet/RuleSet.php new file mode 100644 index 00000000000..124be88db4a --- /dev/null +++ b/lib/php-css-parser/RuleSet/RuleSet.php @@ -0,0 +1,173 @@ +aRules = array(); + $this->iLineNo = $iLineNo; + $this->aComments = array(); + } + + /** + * @return int + */ + public function getLineNo() { + return $this->iLineNo; + } + + public function addRule(Rule $oRule, Rule $oSibling = null) { + $sRule = $oRule->getRule(); + if(!isset($this->aRules[$sRule])) { + $this->aRules[$sRule] = array(); + } + + $iPosition = count($this->aRules[$sRule]); + + if ($oSibling !== null) { + $iSiblingPos = array_search($oSibling, $this->aRules[$sRule], true); + if ($iSiblingPos !== false) { + $iPosition = $iSiblingPos; + } + } + + array_splice($this->aRules[$sRule], $iPosition, 0, array($oRule)); + } + + /** + * 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(). + */ + 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; + } + + /** + * Override all the rules of this set. + * @param array $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 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. + */ + public function getRulesAssoc($mRule = null) { + $aResult = array(); + foreach($this->getRules($mRule) as $oRule) { + $aResult[$oRule->getRule()] = $oRule; + } + return $aResult; + } + + /** + * 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]); + } + } + } + } + + public function __toString() { + return $this->render(new \Sabberworm\CSS\OutputFormat()); + } + + 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 $oOutputFormat->removeLastSemicolon($sResult); + } + + /** + * @param array $aComments Array of comments. + */ + public function addComments(array $aComments) { + $this->aComments = array_merge($this->aComments, $aComments); + } + + /** + * @return array + */ + public function getComments() { + return $this->aComments; + } + + /** + * @param array $aComments Array containing Comment objects. + */ + public function setComments(array $aComments) { + $this->aComments = $aComments; + } + +} diff --git a/lib/php-css-parser/Settings.php b/lib/php-css-parser/Settings.php new file mode 100644 index 00000000000..cb89a8636ba --- /dev/null +++ b/lib/php-css-parser/Settings.php @@ -0,0 +1,54 @@ +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 diff --git a/lib/php-css-parser/Value/CSSFunction.php b/lib/php-css-parser/Value/CSSFunction.php new file mode 100644 index 00000000000..3633abc756c --- /dev/null +++ b/lib/php-css-parser/Value/CSSFunction.php @@ -0,0 +1,40 @@ +getListSeparator(); + $aArguments = $aArguments->getListComponents(); + } + $this->sName = $sName; + $this->iLineNo = $iLineNo; + parent::__construct($aArguments, $sSeparator, $iLineNo); + } + + public function getName() { + return $this->sName; + } + + public function setName($sName) { + $this->sName = $sName; + } + + 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})"; + } + +} \ No newline at end of file diff --git a/lib/php-css-parser/Value/CSSString.php b/lib/php-css-parser/Value/CSSString.php new file mode 100644 index 00000000000..b07000818e2 --- /dev/null +++ b/lib/php-css-parser/Value/CSSString.php @@ -0,0 +1,32 @@ +sString = $sString; + parent::__construct($iLineNo); + } + + public function setString($sString) { + $this->sString = $sString; + } + + public function getString() { + return $this->sString; + } + + 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 diff --git a/lib/php-css-parser/Value/Color.php b/lib/php-css-parser/Value/Color.php new file mode 100644 index 00000000000..e05b924a678 --- /dev/null +++ b/lib/php-css-parser/Value/Color.php @@ -0,0 +1,41 @@ +aComponents; + } + + public function setColor($aColor) { + $this->setName(implode('', array_keys($aColor))); + $this->aComponents = $aColor; + } + + public function getColorDescription() { + return $this->getName(); + } + + public function __toString() { + return $this->render(new \Sabberworm\CSS\OutputFormat()); + } + + 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); + } +} diff --git a/lib/php-css-parser/Value/PrimitiveValue.php b/lib/php-css-parser/Value/PrimitiveValue.php new file mode 100644 index 00000000000..187ce7e6a01 --- /dev/null +++ b/lib/php-css-parser/Value/PrimitiveValue.php @@ -0,0 +1,10 @@ +fSize = floatval($fSize); + $this->sUnit = $sUnit; + $this->bIsColorComponent = $bIsColorComponent; + } + + public function setUnit($sUnit) { + $this->sUnit = $sUnit; + } + + public function getUnit() { + return $this->sUnit; + } + + public function setSize($fSize) { + $this->fSize = floatval($fSize); + } + + public function getSize() { + return $this->fSize; + } + + public function isColorComponent() { + return $this->bIsColorComponent; + } + + /** + * Returns whether the number stored in this Size really represents a size (as in a length of something on screen). + * @return false if the unit an angle, a duration, a frequency or the number is a component in a Color object. + */ + public function isSize() { + if (in_array($this->sUnit, explode('/', self::NON_SIZE_UNITS))) { + return false; + } + return !$this->isColorComponent(); + } + + 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; + } + + public function __toString() { + return $this->render(new \Sabberworm\CSS\OutputFormat()); + } + + 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); + } + +} diff --git a/lib/php-css-parser/Value/URL.php b/lib/php-css-parser/Value/URL.php new file mode 100644 index 00000000000..02cf5812db5 --- /dev/null +++ b/lib/php-css-parser/Value/URL.php @@ -0,0 +1,31 @@ +oURL = $oURL; + } + + public function setURL(CSSString $oURL) { + $this->oURL = $oURL; + } + + 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 diff --git a/lib/php-css-parser/Value/Value.php b/lib/php-css-parser/Value/Value.php new file mode 100644 index 00000000000..5d30bd97fb5 --- /dev/null +++ b/lib/php-css-parser/Value/Value.php @@ -0,0 +1,24 @@ +iLineNo = $iLineNo; + } + + /** + * @return int + */ + public function getLineNo() { + return $this->iLineNo; + } + + //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); +} diff --git a/lib/php-css-parser/Value/ValueList.php b/lib/php-css-parser/Value/ValueList.php new file mode 100644 index 00000000000..5c3d0e4fb0f --- /dev/null +++ b/lib/php-css-parser/Value/ValueList.php @@ -0,0 +1,47 @@ +aComponents = $aComponents; + $this->sSeparator = $sSeparator; + } + + public function addListComponent($mComponent) { + $this->aComponents[] = $mComponent; + } + + public function getListComponents() { + return $this->aComponents; + } + + public function setListComponents($aComponents) { + $this->aComponents = $aComponents; + } + + public function getListSeparator() { + return $this->sSeparator; + } + + public function setListSeparator($sSeparator) { + $this->sSeparator = $sSeparator; + } + + public function __toString() { + return $this->render(new \Sabberworm\CSS\OutputFormat()); + } + + public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) { + return $oOutputFormat->implode($oOutputFormat->spaceBeforeListArgumentSeparator($this->sSeparator) . $this->sSeparator . $oOutputFormat->spaceAfterListArgumentSeparator($this->sSeparator), $this->aComponents); + } + +} diff --git a/lib/php-css-parser/moodle_readme.txt b/lib/php-css-parser/moodle_readme.txt new file mode 100644 index 00000000000..52e303ee5fb --- /dev/null +++ b/lib/php-css-parser/moodle_readme.txt @@ -0,0 +1,11 @@ +PHP CSS Parser +-------------- + +Import git ref: 50a802f562e71236140e2a8903b097c28d6101de + (master) + +Downloaded from: https://github.com/sabberworm/PHP-CSS-Parser + +Import procedure: + +- Copy all the files from the folder 'lib/Sabberworm/CSS/' in this directory. diff --git a/lib/thirdpartylibs.xml b/lib/thirdpartylibs.xml index 25bccc5f42d..61321f68347 100644 --- a/lib/thirdpartylibs.xml +++ b/lib/thirdpartylibs.xml @@ -241,6 +241,12 @@ GPL 1.6.3 + + php-css-parser + PHP-CSS-Parser + MIT + 8.1.0 + rtlcss RTLCSS for PHP