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