This commit is contained in:
emmymayo
2025-02-05 23:15:46 +01:00
commit 7269c99357
16995 changed files with 3389680 additions and 0 deletions
@@ -0,0 +1 @@
<?php
@@ -0,0 +1,46 @@
<?php
namespace MailPoetVendor\Sabberworm\CSS\CSSList;
if (!defined('ABSPATH')) exit;
use MailPoetVendor\Sabberworm\CSS\OutputFormat;
use MailPoetVendor\Sabberworm\CSS\Property\AtRule;
class AtRuleBlockList extends CSSBlockList implements AtRule
{
private $sType;
private $sArgs;
public function __construct($sType, $sArgs = '', $iLineNo = 0)
{
parent::__construct($iLineNo);
$this->sType = $sType;
$this->sArgs = $sArgs;
}
public function atRuleName()
{
return $this->sType;
}
public function atRuleArgs()
{
return $this->sArgs;
}
public function __toString()
{
return $this->render(new OutputFormat());
}
public function render(OutputFormat $oOutputFormat)
{
$sResult = $oOutputFormat->comments($this);
$sResult .= $oOutputFormat->sBeforeAtRuleBlock;
$sArgs = $this->sArgs;
if ($sArgs) {
$sArgs = ' ' . $sArgs;
}
$sResult .= "@{$this->sType}{$sArgs}{$oOutputFormat->spaceBeforeOpeningBrace()}{";
$sResult .= $this->renderListContents($oOutputFormat);
$sResult .= '}';
$sResult .= $oOutputFormat->sAfterAtRuleBlock;
return $sResult;
}
public function isRootList()
{
return \false;
}
}
@@ -0,0 +1,103 @@
<?php
namespace MailPoetVendor\Sabberworm\CSS\CSSList;
if (!defined('ABSPATH')) exit;
use MailPoetVendor\Sabberworm\CSS\Property\Selector;
use MailPoetVendor\Sabberworm\CSS\Rule\Rule;
use MailPoetVendor\Sabberworm\CSS\RuleSet\DeclarationBlock;
use MailPoetVendor\Sabberworm\CSS\RuleSet\RuleSet;
use MailPoetVendor\Sabberworm\CSS\Value\CSSFunction;
use MailPoetVendor\Sabberworm\CSS\Value\Value;
use MailPoetVendor\Sabberworm\CSS\Value\ValueList;
abstract class CSSBlockList extends CSSList
{
public function __construct($iLineNo = 0)
{
parent::__construct($iLineNo);
}
protected function allDeclarationBlocks(array &$aResult)
{
foreach ($this->aContents as $mContent) {
if ($mContent instanceof DeclarationBlock) {
$aResult[] = $mContent;
} elseif ($mContent instanceof CSSBlockList) {
$mContent->allDeclarationBlocks($aResult);
}
}
}
protected function allRuleSets(array &$aResult)
{
foreach ($this->aContents as $mContent) {
if ($mContent instanceof RuleSet) {
$aResult[] = $mContent;
} elseif ($mContent instanceof CSSBlockList) {
$mContent->allRuleSets($aResult);
}
}
}
protected function allValues($oElement, array &$aResult, $sSearchString = null, $bSearchInFunctionArguments = \false)
{
if ($oElement instanceof CSSBlockList) {
foreach ($oElement->getContents() as $oContent) {
$this->allValues($oContent, $aResult, $sSearchString, $bSearchInFunctionArguments);
}
} elseif ($oElement instanceof RuleSet) {
foreach ($oElement->getRules($sSearchString) as $oRule) {
$this->allValues($oRule, $aResult, $sSearchString, $bSearchInFunctionArguments);
}
} elseif ($oElement instanceof Rule) {
$this->allValues($oElement->getValue(), $aResult, $sSearchString, $bSearchInFunctionArguments);
} elseif ($oElement instanceof ValueList) {
if ($bSearchInFunctionArguments || !$oElement instanceof CSSFunction) {
foreach ($oElement->getListComponents() as $mComponent) {
$this->allValues($mComponent, $aResult, $sSearchString, $bSearchInFunctionArguments);
}
}
} else {
// Non-List `Value` or `CSSString` (CSS identifier)
$aResult[] = $oElement;
}
}
protected function allSelectors(array &$aResult, $sSpecificitySearch = null)
{
$aDeclarationBlocks = [];
$this->allDeclarationBlocks($aDeclarationBlocks);
foreach ($aDeclarationBlocks as $oBlock) {
foreach ($oBlock->getSelectors() as $oSelector) {
if ($sSpecificitySearch === null) {
$aResult[] = $oSelector;
} else {
$sComparator = '===';
$aSpecificitySearch = \explode(' ', $sSpecificitySearch);
$iTargetSpecificity = $aSpecificitySearch[0];
if (\count($aSpecificitySearch) > 1) {
$sComparator = $aSpecificitySearch[0];
$iTargetSpecificity = $aSpecificitySearch[1];
}
$iTargetSpecificity = (int) $iTargetSpecificity;
$iSelectorSpecificity = $oSelector->getSpecificity();
$bMatches = \false;
switch ($sComparator) {
case '<=':
$bMatches = $iSelectorSpecificity <= $iTargetSpecificity;
break;
case '<':
$bMatches = $iSelectorSpecificity < $iTargetSpecificity;
break;
case '>=':
$bMatches = $iSelectorSpecificity >= $iTargetSpecificity;
break;
case '>':
$bMatches = $iSelectorSpecificity > $iTargetSpecificity;
break;
default:
$bMatches = $iSelectorSpecificity === $iTargetSpecificity;
break;
}
if ($bMatches) {
$aResult[] = $oSelector;
}
}
}
}
}
}
@@ -0,0 +1,301 @@
<?php
namespace MailPoetVendor\Sabberworm\CSS\CSSList;
if (!defined('ABSPATH')) exit;
use MailPoetVendor\Sabberworm\CSS\Comment\Comment;
use MailPoetVendor\Sabberworm\CSS\Comment\Commentable;
use MailPoetVendor\Sabberworm\CSS\OutputFormat;
use MailPoetVendor\Sabberworm\CSS\Parsing\ParserState;
use MailPoetVendor\Sabberworm\CSS\Parsing\SourceException;
use MailPoetVendor\Sabberworm\CSS\Parsing\UnexpectedEOFException;
use MailPoetVendor\Sabberworm\CSS\Parsing\UnexpectedTokenException;
use MailPoetVendor\Sabberworm\CSS\Property\AtRule;
use MailPoetVendor\Sabberworm\CSS\Property\Charset;
use MailPoetVendor\Sabberworm\CSS\Property\CSSNamespace;
use MailPoetVendor\Sabberworm\CSS\Property\Import;
use MailPoetVendor\Sabberworm\CSS\Property\Selector;
use MailPoetVendor\Sabberworm\CSS\Renderable;
use MailPoetVendor\Sabberworm\CSS\RuleSet\AtRuleSet;
use MailPoetVendor\Sabberworm\CSS\RuleSet\DeclarationBlock;
use MailPoetVendor\Sabberworm\CSS\RuleSet\RuleSet;
use MailPoetVendor\Sabberworm\CSS\Settings;
use MailPoetVendor\Sabberworm\CSS\Value\CSSString;
use MailPoetVendor\Sabberworm\CSS\Value\URL;
use MailPoetVendor\Sabberworm\CSS\Value\Value;
abstract class CSSList implements Renderable, Commentable
{
protected $aComments;
protected $aContents;
protected $iLineNo;
public function __construct($iLineNo = 0)
{
$this->aComments = [];
$this->aContents = [];
$this->iLineNo = $iLineNo;
}
public static function parseList(ParserState $oParserState, CSSList $oList)
{
$bIsRoot = $oList instanceof Document;
if (\is_string($oParserState)) {
$oParserState = new ParserState($oParserState, Settings::create());
}
$bLenientParsing = $oParserState->getSettings()->bLenientParsing;
$aComments = [];
while (!$oParserState->isEnd()) {
$aComments = \array_merge($aComments, $oParserState->consumeWhiteSpace());
$oListItem = null;
if ($bLenientParsing) {
try {
$oListItem = self::parseListItem($oParserState, $oList);
} catch (UnexpectedTokenException $e) {
$oListItem = \false;
}
} else {
$oListItem = self::parseListItem($oParserState, $oList);
}
if ($oListItem === null) {
// List parsing finished
return;
}
if ($oListItem) {
$oListItem->addComments($aComments);
$oList->append($oListItem);
}
$aComments = $oParserState->consumeWhiteSpace();
}
$oList->addComments($aComments);
if (!$bIsRoot && !$bLenientParsing) {
throw new SourceException("Unexpected end of document", $oParserState->currentLine());
}
}
private static function parseListItem(ParserState $oParserState, CSSList $oList)
{
$bIsRoot = $oList instanceof Document;
if ($oParserState->comes('@')) {
$oAtRule = self::parseAtRule($oParserState);
if ($oAtRule instanceof Charset) {
if (!$bIsRoot) {
throw new UnexpectedTokenException('@charset may only occur in root document', '', 'custom', $oParserState->currentLine());
}
if (\count($oList->getContents()) > 0) {
throw new UnexpectedTokenException('@charset must be the first parseable token in a document', '', 'custom', $oParserState->currentLine());
}
$oParserState->setCharset($oAtRule->getCharset());
}
return $oAtRule;
} elseif ($oParserState->comes('}')) {
if ($bIsRoot) {
if ($oParserState->getSettings()->bLenientParsing) {
return DeclarationBlock::parse($oParserState);
} else {
throw new SourceException("Unopened {", $oParserState->currentLine());
}
} else {
// End of list
return null;
}
} else {
return DeclarationBlock::parse($oParserState, $oList);
}
}
private static function parseAtRule(ParserState $oParserState)
{
$oParserState->consume('@');
$sIdentifier = $oParserState->parseIdentifier();
$iIdentifierLineNum = $oParserState->currentLine();
$oParserState->consumeWhiteSpace();
if ($sIdentifier === 'import') {
$oLocation = URL::parse($oParserState);
$oParserState->consumeWhiteSpace();
$sMediaQuery = null;
if (!$oParserState->comes(';')) {
$sMediaQuery = \trim($oParserState->consumeUntil([';', ParserState::EOF]));
}
$oParserState->consumeUntil([';', ParserState::EOF], \true, \true);
return new Import($oLocation, $sMediaQuery ?: null, $iIdentifierLineNum);
} elseif ($sIdentifier === 'charset') {
$oCharsetString = CSSString::parse($oParserState);
$oParserState->consumeWhiteSpace();
$oParserState->consumeUntil([';', ParserState::EOF], \true, \true);
return new Charset($oCharsetString, $iIdentifierLineNum);
} elseif (self::identifierIs($sIdentifier, 'keyframes')) {
$oResult = new KeyFrame($iIdentifierLineNum);
$oResult->setVendorKeyFrame($sIdentifier);
$oResult->setAnimationName(\trim($oParserState->consumeUntil('{', \false, \true)));
CSSList::parseList($oParserState, $oResult);
if ($oParserState->comes('}')) {
$oParserState->consume('}');
}
return $oResult;
} elseif ($sIdentifier === 'namespace') {
$sPrefix = null;
$mUrl = Value::parsePrimitiveValue($oParserState);
if (!$oParserState->comes(';')) {
$sPrefix = $mUrl;
$mUrl = Value::parsePrimitiveValue($oParserState);
}
$oParserState->consumeUntil([';', ParserState::EOF], \true, \true);
if ($sPrefix !== null && !\is_string($sPrefix)) {
throw new UnexpectedTokenException('Wrong namespace prefix', $sPrefix, 'custom', $iIdentifierLineNum);
}
if (!($mUrl instanceof CSSString || $mUrl instanceof URL)) {
throw new UnexpectedTokenException('Wrong namespace url of invalid type', $mUrl, 'custom', $iIdentifierLineNum);
}
return new CSSNamespace($mUrl, $sPrefix, $iIdentifierLineNum);
} else {
// Unknown other at rule (font-face or such)
$sArgs = \trim($oParserState->consumeUntil('{', \false, \true));
if (\substr_count($sArgs, "(") != \substr_count($sArgs, ")")) {
if ($oParserState->getSettings()->bLenientParsing) {
return null;
} else {
throw new SourceException("Unmatched brace count in media query", $oParserState->currentLine());
}
}
$bUseRuleSet = \true;
foreach (\explode('/', AtRule::BLOCK_RULES) as $sBlockRuleName) {
if (self::identifierIs($sIdentifier, $sBlockRuleName)) {
$bUseRuleSet = \false;
break;
}
}
if ($bUseRuleSet) {
$oAtRule = new AtRuleSet($sIdentifier, $sArgs, $iIdentifierLineNum);
RuleSet::parseRuleSet($oParserState, $oAtRule);
} else {
$oAtRule = new AtRuleBlockList($sIdentifier, $sArgs, $iIdentifierLineNum);
CSSList::parseList($oParserState, $oAtRule);
if ($oParserState->comes('}')) {
$oParserState->consume('}');
}
}
return $oAtRule;
}
}
private static function identifierIs($sIdentifier, $sMatch)
{
return \strcasecmp($sIdentifier, $sMatch) === 0 ?: \preg_match("/^(-\\w+-)?{$sMatch}\$/i", $sIdentifier) === 1;
}
public function getLineNo()
{
return $this->iLineNo;
}
public function prepend($oItem)
{
\array_unshift($this->aContents, $oItem);
}
public function append($oItem)
{
$this->aContents[] = $oItem;
}
public function splice($iOffset, $iLength = null, $mReplacement = null)
{
\array_splice($this->aContents, $iOffset, $iLength, $mReplacement);
}
public function remove($oItemToRemove)
{
$iKey = \array_search($oItemToRemove, $this->aContents, \true);
if ($iKey !== \false) {
unset($this->aContents[$iKey]);
return \true;
}
return \false;
}
public function replace($oOldItem, $mNewItem)
{
$iKey = \array_search($oOldItem, $this->aContents, \true);
if ($iKey !== \false) {
if (\is_array($mNewItem)) {
\array_splice($this->aContents, $iKey, 1, $mNewItem);
} else {
\array_splice($this->aContents, $iKey, 1, [$mNewItem]);
}
return \true;
}
return \false;
}
public function setContents(array $aContents)
{
$this->aContents = [];
foreach ($aContents as $content) {
$this->append($content);
}
}
public function removeDeclarationBlockBySelector($mSelector, $bRemoveAll = \false)
{
if ($mSelector instanceof DeclarationBlock) {
$mSelector = $mSelector->getSelectors();
}
if (!\is_array($mSelector)) {
$mSelector = \explode(',', $mSelector);
}
foreach ($mSelector as $iKey => &$mSel) {
if (!$mSel instanceof Selector) {
if (!Selector::isValid($mSel)) {
throw new UnexpectedTokenException("Selector did not match '" . Selector::SELECTOR_VALIDATION_RX . "'.", $mSel, "custom");
}
$mSel = new Selector($mSel);
}
}
foreach ($this->aContents as $iKey => $mItem) {
if (!$mItem instanceof DeclarationBlock) {
continue;
}
if ($mItem->getSelectors() == $mSelector) {
unset($this->aContents[$iKey]);
if (!$bRemoveAll) {
return;
}
}
}
}
public function __toString()
{
return $this->render(new OutputFormat());
}
protected function renderListContents(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;
}
public abstract function isRootList();
public function getContents()
{
return $this->aContents;
}
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;
}
}
@@ -0,0 +1,81 @@
<?php
namespace MailPoetVendor\Sabberworm\CSS\CSSList;
if (!defined('ABSPATH')) exit;
use MailPoetVendor\Sabberworm\CSS\OutputFormat;
use MailPoetVendor\Sabberworm\CSS\Parsing\ParserState;
use MailPoetVendor\Sabberworm\CSS\Parsing\SourceException;
use MailPoetVendor\Sabberworm\CSS\Property\Selector;
use MailPoetVendor\Sabberworm\CSS\RuleSet\DeclarationBlock;
use MailPoetVendor\Sabberworm\CSS\RuleSet\RuleSet;
use MailPoetVendor\Sabberworm\CSS\Value\Value;
class Document extends CSSBlockList
{
public function __construct($iLineNo = 0)
{
parent::__construct($iLineNo);
}
public static function parse(ParserState $oParserState)
{
$oDocument = new Document($oParserState->currentLine());
CSSList::parseList($oParserState, $oDocument);
return $oDocument;
}
public function getAllDeclarationBlocks()
{
$aResult = [];
$this->allDeclarationBlocks($aResult);
return $aResult;
}
public function getAllSelectors()
{
return $this->getAllDeclarationBlocks();
}
public function getAllRuleSets()
{
$aResult = [];
$this->allRuleSets($aResult);
return $aResult;
}
public function getAllValues($mElement = null, $bSearchInFunctionArguments = \false)
{
$sSearchString = null;
if ($mElement === null) {
$mElement = $this;
} elseif (\is_string($mElement)) {
$sSearchString = $mElement;
$mElement = $this;
}
$aResult = [];
$this->allValues($mElement, $aResult, $sSearchString, $bSearchInFunctionArguments);
return $aResult;
}
public function getSelectorsBySpecificity($sSpecificitySearch = null)
{
$aResult = [];
$this->allSelectors($aResult, $sSpecificitySearch);
return $aResult;
}
public function expandShorthands()
{
foreach ($this->getAllDeclarationBlocks() as $oDeclaration) {
$oDeclaration->expandShorthands();
}
}
public function createShorthands()
{
foreach ($this->getAllDeclarationBlocks() as $oDeclaration) {
$oDeclaration->createShorthands();
}
}
public function render(OutputFormat $oOutputFormat = null)
{
if ($oOutputFormat === null) {
$oOutputFormat = new OutputFormat();
}
return $oOutputFormat->comments($this) . $this->renderListContents($oOutputFormat);
}
public function isRootList()
{
return \true;
}
}
@@ -0,0 +1,56 @@
<?php
namespace MailPoetVendor\Sabberworm\CSS\CSSList;
if (!defined('ABSPATH')) exit;
use MailPoetVendor\Sabberworm\CSS\OutputFormat;
use MailPoetVendor\Sabberworm\CSS\Property\AtRule;
class KeyFrame extends CSSList implements AtRule
{
private $vendorKeyFrame;
private $animationName;
public function __construct($iLineNo = 0)
{
parent::__construct($iLineNo);
$this->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 OutputFormat());
}
public function render(OutputFormat $oOutputFormat)
{
$sResult = $oOutputFormat->comments($this);
$sResult .= "@{$this->vendorKeyFrame} {$this->animationName}{$oOutputFormat->spaceBeforeOpeningBrace()}{";
$sResult .= $this->renderListContents($oOutputFormat);
$sResult .= '}';
return $sResult;
}
public function isRootList()
{
return \false;
}
public function atRuleName()
{
return $this->vendorKeyFrame;
}
public function atRuleArgs()
{
return $this->animationName;
}
}
@@ -0,0 +1,35 @@
<?php
namespace MailPoetVendor\Sabberworm\CSS\Comment;
if (!defined('ABSPATH')) exit;
use MailPoetVendor\Sabberworm\CSS\OutputFormat;
use MailPoetVendor\Sabberworm\CSS\Renderable;
class Comment implements Renderable
{
protected $iLineNo;
protected $sComment;
public function __construct($sComment = '', $iLineNo = 0)
{
$this->sComment = $sComment;
$this->iLineNo = $iLineNo;
}
public function getComment()
{
return $this->sComment;
}
public function getLineNo()
{
return $this->iLineNo;
}
public function setComment($sComment)
{
$this->sComment = $sComment;
}
public function __toString()
{
return $this->render(new OutputFormat());
}
public function render(OutputFormat $oOutputFormat)
{
return '/*' . $this->sComment . '*/';
}
}
@@ -0,0 +1,9 @@
<?php
namespace MailPoetVendor\Sabberworm\CSS\Comment;
if (!defined('ABSPATH')) exit;
interface Commentable
{
public function addComments(array $aComments);
public function getComments();
public function setComments(array $aComments);
}
@@ -0,0 +1,130 @@
<?php
namespace MailPoetVendor\Sabberworm\CSS;
if (!defined('ABSPATH')) exit;
class OutputFormat
{
public $sStringQuotingType = '"';
public $bRGBHashNotation = \true;
public $bSemicolonAfterLastRule = \true;
public $sSpaceAfterRuleName = ' ';
public $sSpaceBeforeRules = '';
public $sSpaceAfterRules = '';
public $sSpaceBetweenRules = '';
public $sSpaceBeforeBlocks = '';
public $sSpaceAfterBlocks = '';
public $sSpaceBetweenBlocks = "\n";
public $sBeforeAtRuleBlock = '';
public $sAfterAtRuleBlock = '';
public $sSpaceBeforeSelectorSeparator = '';
public $sSpaceAfterSelectorSeparator = ' ';
public $sSpaceBeforeListArgumentSeparator = '';
public $sSpaceAfterListArgumentSeparator = '';
public $sSpaceBeforeOpeningBrace = ' ';
public $sBeforeDeclarationBlock = '';
public $sAfterDeclarationBlockSelectors = '';
public $sAfterDeclarationBlock = '';
public $sIndentation = "\t";
public $bIgnoreExceptions = \false;
public $bRenderComments = \false;
private $oFormatter = null;
private $oNextLevelFormat = null;
private $iIndentationLevel = 0;
public function __construct()
{
}
public function get($sName)
{
$aVarPrefixes = ['a', 's', 'm', 'b', 'f', 'o', 'c', 'i'];
foreach ($aVarPrefixes as $sPrefix) {
$sFieldName = $sPrefix . \ucfirst($sName);
if (isset($this->{$sFieldName})) {
return $this->{$sFieldName};
}
}
return null;
}
public function set($aNames, $mValue)
{
$aVarPrefixes = ['a', 's', 'm', 'b', 'f', 'o', 'c', 'i'];
if (\is_string($aNames) && \strpos($aNames, '*') !== \false) {
$aNames = [\str_replace('*', 'Before', $aNames), \str_replace('*', 'Between', $aNames), \str_replace('*', 'After', $aNames)];
} elseif (!\is_array($aNames)) {
$aNames = [$aNames];
}
foreach ($aVarPrefixes as $sPrefix) {
$bDidReplace = \false;
foreach ($aNames as $sName) {
$sFieldName = $sPrefix . \ucfirst($sName);
if (isset($this->{$sFieldName})) {
$this->{$sFieldName} = $mValue;
$bDidReplace = \true;
}
}
if ($bDidReplace) {
return $this;
}
}
// Break the chain so the user knows this option is invalid
return \false;
}
public function __call($sMethodName, array $aArguments)
{
if (\strpos($sMethodName, 'set') === 0) {
return $this->set(\substr($sMethodName, 3), $aArguments[0]);
} elseif (\strpos($sMethodName, 'get') === 0) {
return $this->get(\substr($sMethodName, 3));
} elseif (\method_exists(OutputFormatter::class, $sMethodName)) {
return \call_user_func_array([$this->getFormatter(), $sMethodName], $aArguments);
} else {
throw new \Exception('Unknown OutputFormat method called: ' . $sMethodName);
}
}
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()
{
$format = self::create();
$format->set('Space*Rules', "")->set('Space*Blocks', "")->setSpaceAfterRuleName('')->setSpaceBeforeOpeningBrace('')->setSpaceAfterSelectorSeparator('')->setRenderComments(\false);
return $format;
}
public static function createPretty()
{
$format = self::create();
$format->set('Space*Rules', "\n")->set('Space*Blocks', "\n")->setSpaceBetweenBlocks("\n\n")->set('SpaceAfterListArgumentSeparator', ['default' => '', ',' => ' '])->setRenderComments(\true);
return $format;
}
}
@@ -0,0 +1,148 @@
<?php
namespace MailPoetVendor\Sabberworm\CSS;
if (!defined('ABSPATH')) exit;
use MailPoetVendor\Sabberworm\CSS\Comment\Commentable;
use MailPoetVendor\Sabberworm\CSS\Parsing\OutputException;
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 multiple values configured
// depending on the type of object the space applies to
if (\is_array($sSpaceString)) {
if ($sType !== null && isset($sSpaceString[$sType])) {
$sSpaceString = $sSpaceString[$sType];
} else {
$sSpaceString = \reset($sSpaceString);
}
}
return $this->prepareSpace($sSpaceString);
}
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');
}
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();
}
}
public function implode($sSeparator, array $aValues, $bIncreaseLevel = \false)
{
$sResult = '';
$oFormat = $this->oFormat;
if ($bIncreaseLevel) {
$oFormat = $oFormat->nextLevel();
}
$bIsFirst = \true;
foreach ($aValues as $mValue) {
if ($bIsFirst) {
$bIsFirst = \false;
} else {
$sResult .= $sSeparator;
}
if ($mValue instanceof Renderable) {
$sResult .= $mValue->render($oFormat);
} else {
$sResult .= $mValue;
}
}
return $sResult;
}
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);
}
public function comments(Commentable $oCommentable)
{
if (!$this->oFormat->bRenderComments) {
return '';
}
$sResult = '';
$aComments = $oCommentable->getComments();
$iLastCommentIndex = \count($aComments) - 1;
foreach ($aComments as $i => $oComment) {
$sResult .= $oComment->render($this->oFormat);
$sResult .= $i === $iLastCommentIndex ? $this->spaceAfterBlocks() : $this->spaceBetweenBlocks();
}
return $sResult;
}
private function prepareSpace($sSpaceString)
{
return \str_replace("\n", "\n" . $this->indent(), $sSpaceString);
}
private function indent()
{
return \str_repeat($this->oFormat->sIndentation, $this->oFormat->level());
}
}
@@ -0,0 +1,30 @@
<?php
namespace MailPoetVendor\Sabberworm\CSS;
if (!defined('ABSPATH')) exit;
use MailPoetVendor\Sabberworm\CSS\CSSList\Document;
use MailPoetVendor\Sabberworm\CSS\Parsing\ParserState;
use MailPoetVendor\Sabberworm\CSS\Parsing\SourceException;
class Parser
{
private $oParserState;
public function __construct($sText, Settings $oParserSettings = null, $iLineNo = 1)
{
if ($oParserSettings === null) {
$oParserSettings = Settings::create();
}
$this->oParserState = new ParserState($sText, $oParserSettings, $iLineNo);
}
public function setCharset($sCharset)
{
$this->oParserState->setCharset($sCharset);
}
public function getCharset()
{
// Note: The `return` statement is missing here. This is a bug that needs to be fixed.
$this->oParserState->getCharset();
}
public function parse()
{
return Document::parse($this->oParserState);
}
}
@@ -0,0 +1,17 @@
<?php
namespace MailPoetVendor\Sabberworm\CSS\Parsing;
if (!defined('ABSPATH')) exit;
class Anchor
{
private $iPosition;
private $oParserState;
public function __construct($iPosition, ParserState $oParserState)
{
$this->iPosition = $iPosition;
$this->oParserState = $oParserState;
}
public function backtrack()
{
$this->oParserState->setPosition($this->iPosition);
}
}
@@ -0,0 +1,10 @@
<?php
namespace MailPoetVendor\Sabberworm\CSS\Parsing;
if (!defined('ABSPATH')) exit;
class OutputException extends SourceException
{
public function __construct($sMessage, $iLineNo = 0)
{
parent::__construct($sMessage, $iLineNo);
}
}
@@ -0,0 +1,317 @@
<?php
namespace MailPoetVendor\Sabberworm\CSS\Parsing;
if (!defined('ABSPATH')) exit;
use MailPoetVendor\Sabberworm\CSS\Comment\Comment;
use MailPoetVendor\Sabberworm\CSS\Settings;
class ParserState
{
const EOF = null;
private $oParserSettings;
private $sText;
private $aText;
private $iCurrentPosition;
private $sCharset;
private $iLength;
private $iLineNo;
public function __construct($sText, Settings $oParserSettings, $iLineNo = 1)
{
$this->oParserSettings = $oParserSettings;
$this->sText = $sText;
$this->iCurrentPosition = 0;
$this->iLineNo = $iLineNo;
$this->setCharset($this->oParserSettings->sDefaultCharset);
}
public function setCharset($sCharset)
{
$this->sCharset = $sCharset;
$this->aText = $this->strsplit($this->sText);
if (\is_array($this->aText)) {
$this->iLength = \count($this->aText);
}
}
public function getCharset()
{
return $this->sCharset;
}
public function currentLine()
{
return $this->iLineNo;
}
public function currentColumn()
{
return $this->iCurrentPosition;
}
public function getSettings()
{
return $this->oParserSettings;
}
public function anchor()
{
return new Anchor($this->iCurrentPosition, $this);
}
public function setPosition($iPosition)
{
$this->iCurrentPosition = $iPosition;
}
public function parseIdentifier($bIgnoreCase = \true)
{
if ($this->isEnd()) {
throw new UnexpectedEOFException('', '', 'identifier', $this->iLineNo);
}
$sResult = $this->parseCharacter(\true);
if ($sResult === null) {
throw new UnexpectedTokenException($sResult, $this->peek(5), 'identifier', $this->iLineNo);
}
$sCharacter = null;
while (!$this->isEnd() && ($sCharacter = $this->parseCharacter(\true)) !== null) {
if (\preg_match('/[a-zA-Z0-9\\x{00A0}-\\x{FFFF}_-]/Sux', $sCharacter)) {
$sResult .= $sCharacter;
} else {
$sResult .= '\\' . $sCharacter;
}
}
if ($bIgnoreCase) {
$sResult = $this->strtolower($sResult);
}
return $sResult;
}
public function parseCharacter($bIsForIdentifier)
{
if ($this->peek() === '\\') {
if ($bIsForIdentifier && $this->oParserSettings->bLenientParsing && ($this->comes('\\0') || $this->comes('\\9'))) {
// Non-strings can contain \0 or \9 which is an IE hack supported in lenient parsing.
return null;
}
$this->consume('\\');
if ($this->comes('\\n') || $this->comes('\\r')) {
return '';
}
if (\preg_match('/[0-9a-fA-F]/Su', $this->peek()) === 0) {
return $this->consume(1);
}
$sUnicode = $this->consumeExpression('/^[0-9a-fA-F]{1,6}/u', 6);
if ($this->strlen($sUnicode) < 6) {
// Consume whitespace after incomplete unicode escape
if (\preg_match('/\\s/isSu', $this->peek())) {
if ($this->comes('MailPoetVendor\\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;
}
public function consumeWhiteSpace()
{
$aComments = [];
do {
while (\preg_match('/\\s/isSu', $this->peek()) === 1) {
$this->consume(1);
}
if ($this->oParserSettings->bLenientParsing) {
try {
$oComment = $this->consumeComment();
} catch (UnexpectedEOFException $e) {
$this->iCurrentPosition = $this->iLength;
return $aComments;
}
} else {
$oComment = $this->consumeComment();
}
if ($oComment !== \false) {
$aComments[] = $oComment;
}
} while ($oComment !== \false);
return $aComments;
}
public function comes($sString, $bCaseInsensitive = \false)
{
$sPeek = $this->peek(\strlen($sString));
return $sPeek == '' ? \false : $this->streql($sPeek, $sString, $bCaseInsensitive);
}
public function peek($iLength = 1, $iOffset = 0)
{
$iOffset += $this->iCurrentPosition;
if ($iOffset >= $this->iLength) {
return '';
}
return $this->substr($iOffset, $iLength);
}
public function consume($mValue = 1)
{
if (\is_string($mValue)) {
$iLineCount = \substr_count($mValue, "\n");
$iLength = $this->strlen($mValue);
if (!$this->streql($this->substr($this->iCurrentPosition, $iLength), $mValue)) {
throw new UnexpectedTokenException($mValue, $this->peek(\max($iLength, 5)), $this->iLineNo);
}
$this->iLineNo += $iLineCount;
$this->iCurrentPosition += $this->strlen($mValue);
return $mValue;
} else {
if ($this->iCurrentPosition + $mValue > $this->iLength) {
throw new UnexpectedEOFException($mValue, $this->peek(5), 'count', $this->iLineNo);
}
$sResult = $this->substr($this->iCurrentPosition, $mValue);
$iLineCount = \substr_count($sResult, "\n");
$this->iLineNo += $iLineCount;
$this->iCurrentPosition += $mValue;
return $sResult;
}
}
public function consumeExpression($mExpression, $iMaxLength = null)
{
$aMatches = null;
$sInput = $iMaxLength !== null ? $this->peek($iMaxLength) : $this->inputLeft();
if (\preg_match($mExpression, $sInput, $aMatches, \PREG_OFFSET_CAPTURE) === 1) {
return $this->consume($aMatches[0][0]);
}
throw new UnexpectedTokenException($mExpression, $this->peek(5), 'expression', $this->iLineNo);
}
public function consumeComment()
{
$mComment = \false;
if ($this->comes('/*')) {
$iLineNo = $this->iLineNo;
$this->consume(1);
$mComment = '';
while (($char = $this->consume(1)) !== '') {
$mComment .= $char;
if ($this->comes('*/')) {
$this->consume(2);
break;
}
}
}
if ($mComment !== \false) {
// We skip the * which was included in the comment.
return new Comment(\substr($mComment, 1), $iLineNo);
}
return $mComment;
}
public function isEnd()
{
return $this->iCurrentPosition >= $this->iLength;
}
public function consumeUntil($aEnd, $bIncludeEnd = \false, $consumeEnd = \false, array &$comments = [])
{
$aEnd = \is_array($aEnd) ? $aEnd : [$aEnd];
$out = '';
$start = $this->iCurrentPosition;
while (!$this->isEnd()) {
$char = $this->consume(1);
if (\in_array($char, $aEnd)) {
if ($bIncludeEnd) {
$out .= $char;
} elseif (!$consumeEnd) {
$this->iCurrentPosition -= $this->strlen($char);
}
return $out;
}
$out .= $char;
if ($comment = $this->consumeComment()) {
$comments[] = $comment;
}
}
if (\in_array(self::EOF, $aEnd)) {
return $out;
}
$this->iCurrentPosition = $start;
throw new UnexpectedEOFException('One of ("' . \implode('","', $aEnd) . '")', $this->peek(5), 'search', $this->iLineNo);
}
private function inputLeft()
{
return $this->substr($this->iCurrentPosition, -1);
}
public function streql($sString1, $sString2, $bCaseInsensitive = \true)
{
if ($bCaseInsensitive) {
return $this->strtolower($sString1) === $this->strtolower($sString2);
} else {
return $sString1 === $sString2;
}
}
public function backtrack($iAmount)
{
$this->iCurrentPosition -= $iAmount;
}
public function strlen($sString)
{
if ($this->oParserSettings->bMultibyteSupport) {
return \mb_strlen($sString, $this->sCharset);
} else {
return \strlen($sString);
}
}
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 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, -1, \PREG_SPLIT_NO_EMPTY);
} else {
$iLength = \mb_strlen($sString, $this->sCharset);
$aResult = [];
for ($i = 0; $i < $iLength; ++$i) {
$aResult[] = \mb_substr($sString, $i, 1, $this->sCharset);
}
return $aResult;
}
} else {
if ($sString === '') {
return [];
} else {
return \str_split($sString);
}
}
}
private function strpos($sString, $sNeedle, $iOffset)
{
if ($this->oParserSettings->bMultibyteSupport) {
return \mb_strpos($sString, $sNeedle, $iOffset, $this->sCharset);
} else {
return \strpos($sString, $sNeedle, $iOffset);
}
}
}
@@ -0,0 +1,19 @@
<?php
namespace MailPoetVendor\Sabberworm\CSS\Parsing;
if (!defined('ABSPATH')) exit;
class SourceException extends \Exception
{
private $iLineNo;
public function __construct($sMessage, $iLineNo = 0)
{
$this->iLineNo = $iLineNo;
if (!empty($iLineNo)) {
$sMessage .= " [line no: {$iLineNo}]";
}
parent::__construct($sMessage);
}
public function getLineNo()
{
return $this->iLineNo;
}
}
@@ -0,0 +1,6 @@
<?php
namespace MailPoetVendor\Sabberworm\CSS\Parsing;
if (!defined('ABSPATH')) exit;
class UnexpectedEOFException extends UnexpectedTokenException
{
}
@@ -0,0 +1,26 @@
<?php
namespace MailPoetVendor\Sabberworm\CSS\Parsing;
if (!defined('ABSPATH')) exit;
class UnexpectedTokenException extends SourceException
{
private $sExpected;
private $sFound;
private $sMatchType;
public function __construct($sExpected, $sFound, $sMatchType = 'literal', $iLineNo = 0)
{
$this->sExpected = $sExpected;
$this->sFound = $sFound;
$this->sMatchType = $sMatchType;
$sMessage = "Token “{$sExpected}” ({$sMatchType}) not found. Got “{$sFound}”.";
if ($this->sMatchType === 'search') {
$sMessage = "Search for “{$sExpected}” returned no results. Context: “{$sFound}”.";
} elseif ($this->sMatchType === 'count') {
$sMessage = "Next token was expected to have {$sExpected} chars. Context: “{$sFound}”.";
} elseif ($this->sMatchType === 'identifier') {
$sMessage = "Identifier expected. Got “{$sFound}";
} elseif ($this->sMatchType === 'custom') {
$sMessage = \trim("{$sExpected} {$sFound}");
}
parent::__construct($sMessage, $iLineNo);
}
}
@@ -0,0 +1,12 @@
<?php
namespace MailPoetVendor\Sabberworm\CSS\Property;
if (!defined('ABSPATH')) exit;
use MailPoetVendor\Sabberworm\CSS\Comment\Commentable;
use MailPoetVendor\Sabberworm\CSS\Renderable;
interface AtRule extends Renderable, Commentable
{
const BLOCK_RULES = 'media/document/supports/region-style/font-feature-values';
const SET_RULES = 'font-face/counter-style/page/swash/styleset/annotation';
public function atRuleName();
public function atRuleArgs();
}
@@ -0,0 +1,71 @@
<?php
namespace MailPoetVendor\Sabberworm\CSS\Property;
if (!defined('ABSPATH')) exit;
use MailPoetVendor\Sabberworm\CSS\Comment\Comment;
use MailPoetVendor\Sabberworm\CSS\OutputFormat;
class CSSNamespace implements AtRule
{
private $mUrl;
private $sPrefix;
private $iLineNo;
protected $aComments;
public function __construct($mUrl, $sPrefix = null, $iLineNo = 0)
{
$this->mUrl = $mUrl;
$this->sPrefix = $sPrefix;
$this->iLineNo = $iLineNo;
$this->aComments = [];
}
public function getLineNo()
{
return $this->iLineNo;
}
public function __toString()
{
return $this->render(new OutputFormat());
}
public function render(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 = [$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;
}
}
@@ -0,0 +1,59 @@
<?php
namespace MailPoetVendor\Sabberworm\CSS\Property;
if (!defined('ABSPATH')) exit;
use MailPoetVendor\Sabberworm\CSS\Comment\Comment;
use MailPoetVendor\Sabberworm\CSS\OutputFormat;
use MailPoetVendor\Sabberworm\CSS\Value\CSSString;
class Charset implements AtRule
{
private $oCharset;
protected $iLineNo;
protected $aComments;
public function __construct(CSSString $oCharset, $iLineNo = 0)
{
$this->oCharset = $oCharset;
$this->iLineNo = $iLineNo;
$this->aComments = [];
}
public function getLineNo()
{
return $this->iLineNo;
}
public function setCharset($sCharset)
{
$sCharset = $sCharset instanceof CSSString ? $sCharset : new CSSString($sCharset);
$this->oCharset = $sCharset;
}
public function getCharset()
{
return $this->oCharset->getString();
}
public function __toString()
{
return $this->render(new OutputFormat());
}
public function render(OutputFormat $oOutputFormat)
{
return "{$oOutputFormat->comments($this)}@charset {$this->oCharset->render($oOutputFormat)};";
}
public function atRuleName()
{
return 'charset';
}
public function atRuleArgs()
{
return $this->oCharset;
}
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;
}
}
@@ -0,0 +1,68 @@
<?php
namespace MailPoetVendor\Sabberworm\CSS\Property;
if (!defined('ABSPATH')) exit;
use MailPoetVendor\Sabberworm\CSS\Comment\Comment;
use MailPoetVendor\Sabberworm\CSS\OutputFormat;
use MailPoetVendor\Sabberworm\CSS\Value\URL;
class Import implements AtRule
{
private $oLocation;
private $sMediaQuery;
protected $iLineNo;
protected $aComments;
public function __construct(URL $oLocation, $sMediaQuery, $iLineNo = 0)
{
$this->oLocation = $oLocation;
$this->sMediaQuery = $sMediaQuery;
$this->iLineNo = $iLineNo;
$this->aComments = [];
}
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 OutputFormat());
}
public function render(OutputFormat $oOutputFormat)
{
return $oOutputFormat->comments($this) . "@import " . $this->oLocation->render($oOutputFormat) . ($this->sMediaQuery === null ? '' : ' ' . $this->sMediaQuery) . ';';
}
public function atRuleName()
{
return 'import';
}
public function atRuleArgs()
{
$aResult = [$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;
}
public function getMediaQuery()
{
return $this->sMediaQuery;
}
}
@@ -0,0 +1,17 @@
<?php
namespace MailPoetVendor\Sabberworm\CSS\Property;
if (!defined('ABSPATH')) exit;
class KeyframeSelector extends Selector
{
const SELECTOR_VALIDATION_RX = '/
^(
(?:
[a-zA-Z0-9\\x{00A0}-\\x{FFFF}_^$|*="\'~\\[\\]()\\-\\s\\.:#+>]* # any sequence of valid unescaped characters
(?:\\\\.)? # a single escaped character
(?:([\'"]).*?(?<!\\\\)\\2)? # a quoted text like [id="example"]
)*
)|
(\\d+%) # keyframe animation progress percentage (e.g. 50%)
$
/ux';
}
@@ -0,0 +1,79 @@
<?php
namespace MailPoetVendor\Sabberworm\CSS\Property;
if (!defined('ABSPATH')) exit;
class Selector
{
const NON_ID_ATTRIBUTES_AND_PSEUDO_CLASSES_RX = '/
(\\.[\\w]+) # classes
|
\\[(\\w+) # attributes
|
(\\:( # pseudo classes
link|visited|active
|hover|focus
|lang
|target
|enabled|disabled|checked|indeterminate
|root
|nth-child|nth-last-child|nth-of-type|nth-last-of-type
|first-child|last-child|first-of-type|last-of-type
|only-child|only-of-type
|empty|contains
))
/ix';
const ELEMENTS_AND_PSEUDO_ELEMENTS_RX = '/
((^|[\\s\\+\\>\\~]+)[\\w]+ # elements
|
\\:{1,2}( # pseudo-elements
after|before|first-letter|first-line|selection
))
/ix';
const SELECTOR_VALIDATION_RX = '/
^(
(?:
[a-zA-Z0-9\\x{00A0}-\\x{FFFF}_^$|*="\'~\\[\\]()\\-\\s\\.:#+>]* # any sequence of valid unescaped characters
(?:\\\\.)? # a single escaped character
(?:([\'"]).*?(?<!\\\\)\\2)? # a quoted text like [id="example"]
)*
)$
/ux';
private $sSelector;
private $iSpecificity;
public static function isValid($sSelector)
{
return \preg_match(static::SELECTOR_VALIDATION_RX, $sSelector);
}
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;
}
}
@@ -0,0 +1,9 @@
<?php
namespace MailPoetVendor\Sabberworm\CSS;
if (!defined('ABSPATH')) exit;
interface Renderable
{
public function __toString();
public function render(OutputFormat $oOutputFormat);
public function getLineNo();
}
@@ -0,0 +1,223 @@
<?php
namespace MailPoetVendor\Sabberworm\CSS\Rule;
if (!defined('ABSPATH')) exit;
use MailPoetVendor\Sabberworm\CSS\Comment\Comment;
use MailPoetVendor\Sabberworm\CSS\Comment\Commentable;
use MailPoetVendor\Sabberworm\CSS\OutputFormat;
use MailPoetVendor\Sabberworm\CSS\Parsing\ParserState;
use MailPoetVendor\Sabberworm\CSS\Parsing\UnexpectedEOFException;
use MailPoetVendor\Sabberworm\CSS\Parsing\UnexpectedTokenException;
use MailPoetVendor\Sabberworm\CSS\Renderable;
use MailPoetVendor\Sabberworm\CSS\Value\RuleValueList;
use MailPoetVendor\Sabberworm\CSS\Value\Value;
class Rule implements Renderable, Commentable
{
private $sRule;
private $mValue;
private $bIsImportant;
private $aIeHack;
protected $iLineNo;
protected $iColNo;
protected $aComments;
public function __construct($sRule, $iLineNo = 0, $iColNo = 0)
{
$this->sRule = $sRule;
$this->mValue = null;
$this->bIsImportant = \false;
$this->aIeHack = [];
$this->iLineNo = $iLineNo;
$this->iColNo = $iColNo;
$this->aComments = [];
}
public static function parse(ParserState $oParserState)
{
$aComments = $oParserState->consumeWhiteSpace();
$oRule = new Rule($oParserState->parseIdentifier(!$oParserState->comes("--")), $oParserState->currentLine(), $oParserState->currentColumn());
$oRule->setComments($aComments);
$oRule->addComments($oParserState->consumeWhiteSpace());
$oParserState->consume(':');
$oValue = Value::parseValue($oParserState, self::listDelimiterForRule($oRule->getRule()));
$oRule->setValue($oValue);
if ($oParserState->getSettings()->bLenientParsing) {
while ($oParserState->comes('\\')) {
$oParserState->consume('\\');
$oRule->addIeHack($oParserState->consume());
$oParserState->consumeWhiteSpace();
}
}
$oParserState->consumeWhiteSpace();
if ($oParserState->comes('!')) {
$oParserState->consume('!');
$oParserState->consumeWhiteSpace();
$oParserState->consume('important');
$oRule->setIsImportant(\true);
}
$oParserState->consumeWhiteSpace();
while ($oParserState->comes(';')) {
$oParserState->consume(';');
}
$oParserState->consumeWhiteSpace();
return $oRule;
}
private static function listDelimiterForRule($sRule)
{
if (\preg_match('/^font($|-)/', $sRule)) {
return [',', '/', ' '];
}
return [',', ' ', '/'];
}
public function getLineNo()
{
return $this->iLineNo;
}
public function getColNo()
{
return $this->iColNo;
}
public function setPosition($iLine, $iColumn)
{
$this->iColNo = $iColumn;
$this->iLineNo = $iLine;
}
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;
}
public function setValues(array $aSpaceSeparatedValues)
{
$oSpaceSeparatedList = null;
if (\count($aSpaceSeparatedValues) > 1) {
$oSpaceSeparatedList = new RuleValueList(' ', $this->iLineNo);
}
foreach ($aSpaceSeparatedValues as $aCommaSeparatedValues) {
$oCommaSeparatedList = null;
if (\count($aCommaSeparatedValues) > 1) {
$oCommaSeparatedList = new RuleValueList(',', $this->iLineNo);
}
foreach ($aCommaSeparatedValues as $mValue) {
if (!$oSpaceSeparatedList && !$oCommaSeparatedList) {
$this->mValue = $mValue;
return $mValue;
}
if ($oCommaSeparatedList) {
$oCommaSeparatedList->addListComponent($mValue);
} else {
$oSpaceSeparatedList->addListComponent($mValue);
}
}
if (!$oSpaceSeparatedList) {
$this->mValue = $oCommaSeparatedList;
return $oCommaSeparatedList;
} else {
$oSpaceSeparatedList->addListComponent($oCommaSeparatedList);
}
}
$this->mValue = $oSpaceSeparatedList;
return $oSpaceSeparatedList;
}
public function getValues()
{
if (!$this->mValue instanceof RuleValueList) {
return [[$this->mValue]];
}
if ($this->mValue->getListSeparator() === ',') {
return [$this->mValue->getListComponents()];
}
$aResult = [];
foreach ($this->mValue->getListComponents() as $mValue) {
if (!$mValue instanceof RuleValueList || $mValue->getListSeparator() !== ',') {
$aResult[] = [$mValue];
continue;
}
if ($this->mValue->getListSeparator() === ' ' || \count($aResult) === 0) {
$aResult[] = [];
}
foreach ($mValue->getListComponents() as $mValue) {
$aResult[\count($aResult) - 1][] = $mValue;
}
}
return $aResult;
}
public function addValue($mValue, $sType = ' ')
{
if (!\is_array($mValue)) {
$mValue = [$mValue];
}
if (!$this->mValue instanceof RuleValueList || $this->mValue->getListSeparator() !== $sType) {
$mCurrentValue = $this->mValue;
$this->mValue = new RuleValueList($sType, $this->iLineNo);
if ($mCurrentValue) {
$this->mValue->addListComponent($mCurrentValue);
}
}
foreach ($mValue as $mValueItem) {
$this->mValue->addListComponent($mValueItem);
}
}
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 OutputFormat());
}
public function render(OutputFormat $oOutputFormat)
{
$sResult = "{$oOutputFormat->comments($this)}{$this->sRule}:{$oOutputFormat->spaceAfterRuleName()}";
if ($this->mValue instanceof Value) {
// Can also be a ValueList
$sResult .= $this->mValue->render($oOutputFormat);
} else {
$sResult .= $this->mValue;
}
if (!empty($this->aIeHack)) {
$sResult .= ' \\' . \implode('\\', $this->aIeHack);
}
if ($this->bIsImportant) {
$sResult .= ' !important';
}
$sResult .= ';';
return $sResult;
}
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;
}
}
@@ -0,0 +1,40 @@
<?php
namespace MailPoetVendor\Sabberworm\CSS\RuleSet;
if (!defined('ABSPATH')) exit;
use MailPoetVendor\Sabberworm\CSS\OutputFormat;
use MailPoetVendor\Sabberworm\CSS\Property\AtRule;
class AtRuleSet extends RuleSet implements AtRule
{
private $sType;
private $sArgs;
public function __construct($sType, $sArgs = '', $iLineNo = 0)
{
parent::__construct($iLineNo);
$this->sType = $sType;
$this->sArgs = $sArgs;
}
public function atRuleName()
{
return $this->sType;
}
public function atRuleArgs()
{
return $this->sArgs;
}
public function __toString()
{
return $this->render(new OutputFormat());
}
public function render(OutputFormat $oOutputFormat)
{
$sResult = $oOutputFormat->comments($this);
$sArgs = $this->sArgs;
if ($sArgs) {
$sArgs = ' ' . $sArgs;
}
$sResult .= "@{$this->sType}{$sArgs}{$oOutputFormat->spaceBeforeOpeningBrace()}{";
$sResult .= $this->renderRules($oOutputFormat);
$sResult .= '}';
return $sResult;
}
}
@@ -0,0 +1,564 @@
<?php
namespace MailPoetVendor\Sabberworm\CSS\RuleSet;
if (!defined('ABSPATH')) exit;
use MailPoetVendor\Sabberworm\CSS\CSSList\CSSList;
use MailPoetVendor\Sabberworm\CSS\CSSList\KeyFrame;
use MailPoetVendor\Sabberworm\CSS\OutputFormat;
use MailPoetVendor\Sabberworm\CSS\Parsing\OutputException;
use MailPoetVendor\Sabberworm\CSS\Parsing\ParserState;
use MailPoetVendor\Sabberworm\CSS\Parsing\UnexpectedEOFException;
use MailPoetVendor\Sabberworm\CSS\Parsing\UnexpectedTokenException;
use MailPoetVendor\Sabberworm\CSS\Property\KeyframeSelector;
use MailPoetVendor\Sabberworm\CSS\Property\Selector;
use MailPoetVendor\Sabberworm\CSS\Rule\Rule;
use MailPoetVendor\Sabberworm\CSS\Value\Color;
use MailPoetVendor\Sabberworm\CSS\Value\RuleValueList;
use MailPoetVendor\Sabberworm\CSS\Value\Size;
use MailPoetVendor\Sabberworm\CSS\Value\URL;
use MailPoetVendor\Sabberworm\CSS\Value\Value;
class DeclarationBlock extends RuleSet
{
private $aSelectors;
public function __construct($iLineNo = 0)
{
parent::__construct($iLineNo);
$this->aSelectors = [];
}
public static function parse(ParserState $oParserState, $oList = null)
{
$aComments = [];
$oResult = new DeclarationBlock($oParserState->currentLine());
try {
$aSelectorParts = [];
$sStringWrapperChar = \false;
do {
$aSelectorParts[] = $oParserState->consume(1) . $oParserState->consumeUntil(['{', '}', '\'', '"'], \false, \false, $aComments);
if (\in_array($oParserState->peek(), ['\'', '"']) && \substr(\end($aSelectorParts), -1) != "\\") {
if ($sStringWrapperChar === \false) {
$sStringWrapperChar = $oParserState->peek();
} elseif ($sStringWrapperChar == $oParserState->peek()) {
$sStringWrapperChar = \false;
}
}
} while (!\in_array($oParserState->peek(), ['{', '}']) || $sStringWrapperChar !== \false);
$oResult->setSelectors(\implode('', $aSelectorParts), $oList);
if ($oParserState->comes('{')) {
$oParserState->consume(1);
}
} catch (UnexpectedTokenException $e) {
if ($oParserState->getSettings()->bLenientParsing) {
if (!$oParserState->comes('}')) {
$oParserState->consumeUntil('}', \false, \true);
}
return \false;
} else {
throw $e;
}
}
$oResult->setComments($aComments);
RuleSet::parseRuleSet($oParserState, $oResult);
return $oResult;
}
public function setSelectors($mSelector, $oList = null)
{
if (\is_array($mSelector)) {
$this->aSelectors = $mSelector;
} else {
$this->aSelectors = \explode(',', $mSelector);
}
foreach ($this->aSelectors as $iKey => $mSelector) {
if (!$mSelector instanceof Selector) {
if ($oList === null || !$oList instanceof KeyFrame) {
if (!Selector::isValid($mSelector)) {
throw new UnexpectedTokenException("Selector did not match '" . Selector::SELECTOR_VALIDATION_RX . "'.", $mSelector, "custom");
}
$this->aSelectors[$iKey] = new Selector($mSelector);
} else {
if (!KeyframeSelector::isValid($mSelector)) {
throw new UnexpectedTokenException("Selector did not match '" . KeyframeSelector::SELECTOR_VALIDATION_RX . "'.", $mSelector, "custom");
}
$this->aSelectors[$iKey] = new KeyframeSelector($mSelector);
}
}
}
}
public function removeSelector($mSelector)
{
if ($mSelector instanceof Selector) {
$mSelector = $mSelector->getSelector();
}
foreach ($this->aSelectors as $iKey => $oSelector) {
if ($oSelector->getSelector() === $mSelector) {
unset($this->aSelectors[$iKey]);
return \true;
}
}
return \false;
}
public function getSelector()
{
return $this->getSelectors();
}
public function setSelector($mSelector, $oList = null)
{
$this->setSelectors($mSelector, $oList);
}
public function getSelectors()
{
return $this->aSelectors;
}
public function expandShorthands()
{
// border must be expanded before dimensions
$this->expandBorderShorthand();
$this->expandDimensionsShorthand();
$this->expandFontShorthand();
$this->expandBackgroundShorthand();
$this->expandListStyleShorthand();
}
public function createShorthands()
{
$this->createBackgroundShorthand();
$this->createDimensionsShorthand();
// border must be shortened after dimensions
$this->createBorderShorthand();
$this->createFontShorthand();
$this->createListStyleShorthand();
}
public function expandBorderShorthand()
{
$aBorderRules = ['border', 'border-left', 'border-right', 'border-top', 'border-bottom'];
$aBorderSizes = ['thin', 'medium', 'thick'];
$aRules = $this->getRulesAssoc();
foreach ($aBorderRules as $sBorderRule) {
if (!isset($aRules[$sBorderRule])) {
continue;
}
$oRule = $aRules[$sBorderRule];
$mRuleValue = $oRule->getValue();
$aValues = [];
if (!$mRuleValue instanceof RuleValueList) {
$aValues[] = $mRuleValue;
} else {
$aValues = $mRuleValue->getListComponents();
}
foreach ($aValues as $mValue) {
if ($mValue instanceof Value) {
$mNewValue = clone $mValue;
} else {
$mNewValue = $mValue;
}
if ($mValue instanceof Size) {
$sNewRuleName = $sBorderRule . "-width";
} elseif ($mValue instanceof Color) {
$sNewRuleName = $sBorderRule . "-color";
} else {
if (\in_array($mValue, $aBorderSizes)) {
$sNewRuleName = $sBorderRule . "-width";
} else {
$sNewRuleName = $sBorderRule . "-style";
}
}
$oNewRule = new Rule($sNewRuleName, $oRule->getLineNo(), $oRule->getColNo());
$oNewRule->setIsImportant($oRule->getIsImportant());
$oNewRule->addValue([$mNewValue]);
$this->addRule($oNewRule);
}
$this->removeRule($sBorderRule);
}
}
public function expandDimensionsShorthand()
{
$aExpansions = ['margin' => 'margin-%s', 'padding' => 'padding-%s', 'border-color' => 'border-%s-color', 'border-style' => 'border-%s-style', 'border-width' => 'border-%s-width'];
$aRules = $this->getRulesAssoc();
foreach ($aExpansions as $sProperty => $sExpanded) {
if (!isset($aRules[$sProperty])) {
continue;
}
$oRule = $aRules[$sProperty];
$mRuleValue = $oRule->getValue();
$aValues = [];
if (!$mRuleValue instanceof RuleValueList) {
$aValues[] = $mRuleValue;
} else {
$aValues = $mRuleValue->getListComponents();
}
$top = $right = $bottom = $left = null;
switch (\count($aValues)) {
case 1:
$top = $right = $bottom = $left = $aValues[0];
break;
case 2:
$top = $bottom = $aValues[0];
$left = $right = $aValues[1];
break;
case 3:
$top = $aValues[0];
$left = $right = $aValues[1];
$bottom = $aValues[2];
break;
case 4:
$top = $aValues[0];
$right = $aValues[1];
$bottom = $aValues[2];
$left = $aValues[3];
break;
}
foreach (['top', 'right', 'bottom', 'left'] as $sPosition) {
$oNewRule = new Rule(\sprintf($sExpanded, $sPosition), $oRule->getLineNo(), $oRule->getColNo());
$oNewRule->setIsImportant($oRule->getIsImportant());
$oNewRule->addValue(${$sPosition});
$this->addRule($oNewRule);
}
$this->removeRule($sProperty);
}
}
public function expandFontShorthand()
{
$aRules = $this->getRulesAssoc();
if (!isset($aRules['font'])) {
return;
}
$oRule = $aRules['font'];
// reset properties to 'normal' per http://www.w3.org/TR/21/fonts.html#font-shorthand
$aFontProperties = ['font-style' => 'normal', 'font-variant' => 'normal', 'font-weight' => 'normal', 'font-size' => 'normal', 'line-height' => 'normal'];
$mRuleValue = $oRule->getValue();
$aValues = [];
if (!$mRuleValue instanceof RuleValueList) {
$aValues[] = $mRuleValue;
} else {
$aValues = $mRuleValue->getListComponents();
}
foreach ($aValues as $mValue) {
if (!$mValue instanceof Value) {
$mValue = \mb_strtolower($mValue);
}
if (\in_array($mValue, ['normal', 'inherit'])) {
foreach (['font-style', 'font-weight', 'font-variant'] as $sProperty) {
if (!isset($aFontProperties[$sProperty])) {
$aFontProperties[$sProperty] = $mValue;
}
}
} elseif (\in_array($mValue, ['italic', 'oblique'])) {
$aFontProperties['font-style'] = $mValue;
} elseif ($mValue == 'small-caps') {
$aFontProperties['font-variant'] = $mValue;
} elseif (\in_array($mValue, ['bold', 'bolder', 'lighter']) || $mValue instanceof Size && \in_array($mValue->getSize(), \range(100, 900, 100))) {
$aFontProperties['font-weight'] = $mValue;
} elseif ($mValue instanceof RuleValueList && $mValue->getListSeparator() == '/') {
list($oSize, $oHeight) = $mValue->getListComponents();
$aFontProperties['font-size'] = $oSize;
$aFontProperties['line-height'] = $oHeight;
} elseif ($mValue instanceof Size && $mValue->getUnit() !== null) {
$aFontProperties['font-size'] = $mValue;
} else {
$aFontProperties['font-family'] = $mValue;
}
}
foreach ($aFontProperties as $sProperty => $mValue) {
$oNewRule = new Rule($sProperty, $oRule->getLineNo(), $oRule->getColNo());
$oNewRule->addValue($mValue);
$oNewRule->setIsImportant($oRule->getIsImportant());
$this->addRule($oNewRule);
}
$this->removeRule('font');
}
public function expandBackgroundShorthand()
{
$aRules = $this->getRulesAssoc();
if (!isset($aRules['background'])) {
return;
}
$oRule = $aRules['background'];
$aBgProperties = ['background-color' => ['transparent'], 'background-image' => ['none'], 'background-repeat' => ['repeat'], 'background-attachment' => ['scroll'], 'background-position' => [new Size(0, '%', null, \false, $this->iLineNo), new Size(0, '%', null, \false, $this->iLineNo)]];
$mRuleValue = $oRule->getValue();
$aValues = [];
if (!$mRuleValue instanceof RuleValueList) {
$aValues[] = $mRuleValue;
} else {
$aValues = $mRuleValue->getListComponents();
}
if (\count($aValues) == 1 && $aValues[0] == 'inherit') {
foreach ($aBgProperties as $sProperty => $mValue) {
$oNewRule = new Rule($sProperty, $oRule->getLineNo(), $oRule->getColNo());
$oNewRule->addValue('inherit');
$oNewRule->setIsImportant($oRule->getIsImportant());
$this->addRule($oNewRule);
}
$this->removeRule('background');
return;
}
$iNumBgPos = 0;
foreach ($aValues as $mValue) {
if (!$mValue instanceof Value) {
$mValue = \mb_strtolower($mValue);
}
if ($mValue instanceof URL) {
$aBgProperties['background-image'] = $mValue;
} elseif ($mValue instanceof Color) {
$aBgProperties['background-color'] = $mValue;
} elseif (\in_array($mValue, ['scroll', 'fixed'])) {
$aBgProperties['background-attachment'] = $mValue;
} elseif (\in_array($mValue, ['repeat', 'no-repeat', 'repeat-x', 'repeat-y'])) {
$aBgProperties['background-repeat'] = $mValue;
} elseif (\in_array($mValue, ['left', 'center', 'right', 'top', 'bottom']) || $mValue instanceof Size) {
if ($iNumBgPos == 0) {
$aBgProperties['background-position'][0] = $mValue;
$aBgProperties['background-position'][1] = 'center';
} else {
$aBgProperties['background-position'][$iNumBgPos] = $mValue;
}
$iNumBgPos++;
}
}
foreach ($aBgProperties as $sProperty => $mValue) {
$oNewRule = new Rule($sProperty, $oRule->getLineNo(), $oRule->getColNo());
$oNewRule->setIsImportant($oRule->getIsImportant());
$oNewRule->addValue($mValue);
$this->addRule($oNewRule);
}
$this->removeRule('background');
}
public function expandListStyleShorthand()
{
$aListProperties = ['list-style-type' => 'disc', 'list-style-position' => 'outside', 'list-style-image' => 'none'];
$aListStyleTypes = ['none', 'disc', 'circle', 'square', 'decimal-leading-zero', 'decimal', 'lower-roman', 'upper-roman', 'lower-greek', 'lower-alpha', 'lower-latin', 'upper-alpha', 'upper-latin', 'hebrew', 'armenian', 'georgian', 'cjk-ideographic', 'hiragana', 'hira-gana-iroha', 'katakana-iroha', 'katakana'];
$aListStylePositions = ['inside', 'outside'];
$aRules = $this->getRulesAssoc();
if (!isset($aRules['list-style'])) {
return;
}
$oRule = $aRules['list-style'];
$mRuleValue = $oRule->getValue();
$aValues = [];
if (!$mRuleValue instanceof RuleValueList) {
$aValues[] = $mRuleValue;
} else {
$aValues = $mRuleValue->getListComponents();
}
if (\count($aValues) == 1 && $aValues[0] == 'inherit') {
foreach ($aListProperties as $sProperty => $mValue) {
$oNewRule = new Rule($sProperty, $oRule->getLineNo(), $oRule->getColNo());
$oNewRule->addValue('inherit');
$oNewRule->setIsImportant($oRule->getIsImportant());
$this->addRule($oNewRule);
}
$this->removeRule('list-style');
return;
}
foreach ($aValues as $mValue) {
if (!$mValue instanceof Value) {
$mValue = \mb_strtolower($mValue);
}
if ($mValue instanceof Url) {
$aListProperties['list-style-image'] = $mValue;
} elseif (\in_array($mValue, $aListStyleTypes)) {
$aListProperties['list-style-types'] = $mValue;
} elseif (\in_array($mValue, $aListStylePositions)) {
$aListProperties['list-style-position'] = $mValue;
}
}
foreach ($aListProperties as $sProperty => $mValue) {
$oNewRule = new Rule($sProperty, $oRule->getLineNo(), $oRule->getColNo());
$oNewRule->setIsImportant($oRule->getIsImportant());
$oNewRule->addValue($mValue);
$this->addRule($oNewRule);
}
$this->removeRule('list-style');
}
public function createShorthandProperties(array $aProperties, $sShorthand)
{
$aRules = $this->getRulesAssoc();
$oRule = null;
$aNewValues = [];
foreach ($aProperties as $sProperty) {
if (!isset($aRules[$sProperty])) {
continue;
}
$oRule = $aRules[$sProperty];
if (!$oRule->getIsImportant()) {
$mRuleValue = $oRule->getValue();
$aValues = [];
if (!$mRuleValue instanceof RuleValueList) {
$aValues[] = $mRuleValue;
} else {
$aValues = $mRuleValue->getListComponents();
}
foreach ($aValues as $mValue) {
$aNewValues[] = $mValue;
}
$this->removeRule($sProperty);
}
}
if ($aNewValues !== [] && $oRule instanceof Rule) {
$oNewRule = new Rule($sShorthand, $oRule->getLineNo(), $oRule->getColNo());
foreach ($aNewValues as $mValue) {
$oNewRule->addValue($mValue);
}
$this->addRule($oNewRule);
}
}
public function createBackgroundShorthand()
{
$aProperties = ['background-color', 'background-image', 'background-repeat', 'background-position', 'background-attachment'];
$this->createShorthandProperties($aProperties, 'background');
}
public function createListStyleShorthand()
{
$aProperties = ['list-style-type', 'list-style-position', 'list-style-image'];
$this->createShorthandProperties($aProperties, 'list-style');
}
public function createBorderShorthand()
{
$aProperties = ['border-width', 'border-style', 'border-color'];
$this->createShorthandProperties($aProperties, 'border');
}
public function createDimensionsShorthand()
{
$aPositions = ['top', 'right', 'bottom', 'left'];
$aExpansions = ['margin' => 'margin-%s', 'padding' => 'padding-%s', 'border-color' => 'border-%s-color', 'border-style' => 'border-%s-style', 'border-width' => 'border-%s-width'];
$aRules = $this->getRulesAssoc();
foreach ($aExpansions as $sProperty => $sExpanded) {
$aFoldable = [];
foreach ($aRules as $sRuleName => $oRule) {
foreach ($aPositions as $sPosition) {
if ($sRuleName == \sprintf($sExpanded, $sPosition)) {
$aFoldable[$sRuleName] = $oRule;
}
}
}
// All four dimensions must be present
if (\count($aFoldable) == 4) {
$aValues = [];
foreach ($aPositions as $sPosition) {
$oRule = $aRules[\sprintf($sExpanded, $sPosition)];
$mRuleValue = $oRule->getValue();
$aRuleValues = [];
if (!$mRuleValue instanceof RuleValueList) {
$aRuleValues[] = $mRuleValue;
} else {
$aRuleValues = $mRuleValue->getListComponents();
}
$aValues[$sPosition] = $aRuleValues;
}
$oNewRule = new Rule($sProperty, $oRule->getLineNo(), $oRule->getColNo());
if ((string) $aValues['left'][0] == (string) $aValues['right'][0]) {
if ((string) $aValues['top'][0] == (string) $aValues['bottom'][0]) {
if ((string) $aValues['top'][0] == (string) $aValues['left'][0]) {
// All 4 sides are equal
$oNewRule->addValue($aValues['top']);
} else {
// Top and bottom are equal, left and right are equal
$oNewRule->addValue($aValues['top']);
$oNewRule->addValue($aValues['left']);
}
} else {
// Only left and right are equal
$oNewRule->addValue($aValues['top']);
$oNewRule->addValue($aValues['left']);
$oNewRule->addValue($aValues['bottom']);
}
} else {
// No sides are equal
$oNewRule->addValue($aValues['top']);
$oNewRule->addValue($aValues['left']);
$oNewRule->addValue($aValues['bottom']);
$oNewRule->addValue($aValues['right']);
}
$this->addRule($oNewRule);
foreach ($aPositions as $sPosition) {
$this->removeRule(\sprintf($sExpanded, $sPosition));
}
}
}
}
public function createFontShorthand()
{
$aFontProperties = ['font-style', 'font-variant', 'font-weight', 'font-size', 'line-height', 'font-family'];
$aRules = $this->getRulesAssoc();
if (!isset($aRules['font-size']) || !isset($aRules['font-family'])) {
return;
}
$oOldRule = isset($aRules['font-size']) ? $aRules['font-size'] : $aRules['font-family'];
$oNewRule = new Rule('font', $oOldRule->getLineNo(), $oOldRule->getColNo());
unset($oOldRule);
foreach (['font-style', 'font-variant', 'font-weight'] as $sProperty) {
if (isset($aRules[$sProperty])) {
$oRule = $aRules[$sProperty];
$mRuleValue = $oRule->getValue();
$aValues = [];
if (!$mRuleValue instanceof RuleValueList) {
$aValues[] = $mRuleValue;
} else {
$aValues = $mRuleValue->getListComponents();
}
if ($aValues[0] !== 'normal') {
$oNewRule->addValue($aValues[0]);
}
}
}
// Get the font-size value
$oRule = $aRules['font-size'];
$mRuleValue = $oRule->getValue();
$aFSValues = [];
if (!$mRuleValue instanceof RuleValueList) {
$aFSValues[] = $mRuleValue;
} else {
$aFSValues = $mRuleValue->getListComponents();
}
// But wait to know if we have line-height to add it
if (isset($aRules['line-height'])) {
$oRule = $aRules['line-height'];
$mRuleValue = $oRule->getValue();
$aLHValues = [];
if (!$mRuleValue instanceof RuleValueList) {
$aLHValues[] = $mRuleValue;
} else {
$aLHValues = $mRuleValue->getListComponents();
}
if ($aLHValues[0] !== 'normal') {
$val = new RuleValueList('/', $this->iLineNo);
$val->addListComponent($aFSValues[0]);
$val->addListComponent($aLHValues[0]);
$oNewRule->addValue($val);
}
} else {
$oNewRule->addValue($aFSValues[0]);
}
$oRule = $aRules['font-family'];
$mRuleValue = $oRule->getValue();
$aFFValues = [];
if (!$mRuleValue instanceof RuleValueList) {
$aFFValues[] = $mRuleValue;
} else {
$aFFValues = $mRuleValue->getListComponents();
}
$oFFValue = new RuleValueList(',', $this->iLineNo);
$oFFValue->setListComponents($aFFValues);
$oNewRule->addValue($oFFValue);
$this->addRule($oNewRule);
foreach ($aFontProperties as $sProperty) {
$this->removeRule($sProperty);
}
}
public function __toString()
{
return $this->render(new OutputFormat());
}
public function render(OutputFormat $oOutputFormat)
{
$sResult = $oOutputFormat->comments($this);
if (\count($this->aSelectors) === 0) {
// If all the selectors have been removed, this declaration block becomes invalid
throw new OutputException("Attempt to print declaration block with missing selector", $this->iLineNo);
}
$sResult .= $oOutputFormat->sBeforeDeclarationBlock;
$sResult .= $oOutputFormat->implode($oOutputFormat->spaceBeforeSelectorSeparator() . ',' . $oOutputFormat->spaceAfterSelectorSeparator(), $this->aSelectors);
$sResult .= $oOutputFormat->sAfterDeclarationBlockSelectors;
$sResult .= $oOutputFormat->spaceBeforeOpeningBrace() . '{';
$sResult .= $this->renderRules($oOutputFormat);
$sResult .= '}';
$sResult .= $oOutputFormat->sAfterDeclarationBlock;
return $sResult;
}
}
@@ -0,0 +1,190 @@
<?php
namespace MailPoetVendor\Sabberworm\CSS\RuleSet;
if (!defined('ABSPATH')) exit;
use MailPoetVendor\Sabberworm\CSS\Comment\Comment;
use MailPoetVendor\Sabberworm\CSS\Comment\Commentable;
use MailPoetVendor\Sabberworm\CSS\OutputFormat;
use MailPoetVendor\Sabberworm\CSS\Parsing\ParserState;
use MailPoetVendor\Sabberworm\CSS\Parsing\UnexpectedEOFException;
use MailPoetVendor\Sabberworm\CSS\Parsing\UnexpectedTokenException;
use MailPoetVendor\Sabberworm\CSS\Renderable;
use MailPoetVendor\Sabberworm\CSS\Rule\Rule;
abstract class RuleSet implements Renderable, Commentable
{
private $aRules;
protected $iLineNo;
protected $aComments;
public function __construct($iLineNo = 0)
{
$this->aRules = [];
$this->iLineNo = $iLineNo;
$this->aComments = [];
}
public static function parseRuleSet(ParserState $oParserState, RuleSet $oRuleSet)
{
while ($oParserState->comes(';')) {
$oParserState->consume(';');
}
while (!$oParserState->comes('}')) {
$oRule = null;
if ($oParserState->getSettings()->bLenientParsing) {
try {
$oRule = Rule::parse($oParserState);
} catch (UnexpectedTokenException $e) {
try {
$sConsume = $oParserState->consumeUntil(["\n", ";", '}'], \true);
// We need to “unfind” the matches to the end of the ruleSet as this will be matched later
if ($oParserState->streql(\substr($sConsume, -1), '}')) {
$oParserState->backtrack(1);
} else {
while ($oParserState->comes(';')) {
$oParserState->consume(';');
}
}
} catch (UnexpectedTokenException $e) {
// Weve reached the end of the document. Just close the RuleSet.
return;
}
}
} else {
$oRule = Rule::parse($oParserState);
}
if ($oRule) {
$oRuleSet->addRule($oRule);
}
}
$oParserState->consume('}');
}
public function getLineNo()
{
return $this->iLineNo;
}
public function addRule(Rule $oRule, Rule $oSibling = null)
{
$sRule = $oRule->getRule();
if (!isset($this->aRules[$sRule])) {
$this->aRules[$sRule] = [];
}
$iPosition = \count($this->aRules[$sRule]);
if ($oSibling !== null) {
$iSiblingPos = \array_search($oSibling, $this->aRules[$sRule], \true);
if ($iSiblingPos !== \false) {
$iPosition = $iSiblingPos;
$oRule->setPosition($oSibling->getLineNo(), $oSibling->getColNo() - 1);
}
}
if ($oRule->getLineNo() === 0 && $oRule->getColNo() === 0) {
//this node is added manually, give it the next best line
$rules = $this->getRules();
$pos = \count($rules);
if ($pos > 0) {
$last = $rules[$pos - 1];
$oRule->setPosition($last->getLineNo() + 1, 0);
}
}
\array_splice($this->aRules[$sRule], $iPosition, 0, [$oRule]);
}
public function getRules($mRule = null)
{
if ($mRule instanceof Rule) {
$mRule = $mRule->getRule();
}
$aResult = [];
foreach ($this->aRules as $sName => $aRules) {
// Either no search rule is given or the search rule matches the found rule exactly
// or the search rule ends in “-” and the found rule starts with the search rule.
if (!$mRule || $sName === $mRule || \strrpos($mRule, '-') === \strlen($mRule) - \strlen('-') && (\strpos($sName, $mRule) === 0 || $sName === \substr($mRule, 0, -1))) {
$aResult = \array_merge($aResult, $aRules);
}
}
\usort($aResult, function (Rule $first, Rule $second) {
if ($first->getLineNo() === $second->getLineNo()) {
return $first->getColNo() - $second->getColNo();
}
return $first->getLineNo() - $second->getLineNo();
});
return $aResult;
}
public function setRules(array $aRules)
{
$this->aRules = [];
foreach ($aRules as $rule) {
$this->addRule($rule);
}
}
public function getRulesAssoc($mRule = null)
{
$aResult = [];
foreach ($this->getRules($mRule) as $oRule) {
$aResult[$oRule->getRule()] = $oRule;
}
return $aResult;
}
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 OutputFormat());
}
protected function renderRules(OutputFormat $oOutputFormat)
{
$sResult = '';
$bIsFirst = \true;
$oNextLevel = $oOutputFormat->nextLevel();
foreach ($this->aRules as $aRules) {
foreach ($aRules as $oRule) {
$sRendered = $oNextLevel->safely(function () use($oRule, $oNextLevel) {
return $oRule->render($oNextLevel);
});
if ($sRendered === null) {
continue;
}
if ($bIsFirst) {
$bIsFirst = \false;
$sResult .= $oNextLevel->spaceBeforeRules();
} else {
$sResult .= $oNextLevel->spaceBetweenRules();
}
$sResult .= $sRendered;
}
}
if (!$bIsFirst) {
// Had some output
$sResult .= $oOutputFormat->spaceAfterRules();
}
return $oOutputFormat->removeLastSemicolon($sResult);
}
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;
}
}
@@ -0,0 +1,36 @@
<?php
namespace MailPoetVendor\Sabberworm\CSS;
if (!defined('ABSPATH')) exit;
class Settings
{
public $bMultibyteSupport;
public $sDefaultCharset = 'utf-8';
public $bLenientParsing = \true;
private function __construct()
{
$this->bMultibyteSupport = \extension_loaded('mbstring');
}
public static function create()
{
return new Settings();
}
public function withMultibyteSupport($bMultibyteSupport = \true)
{
$this->bMultibyteSupport = $bMultibyteSupport;
return $this;
}
public function withDefaultCharset($sDefaultCharset)
{
$this->sDefaultCharset = $sDefaultCharset;
return $this;
}
public function withLenientParsing($bLenientParsing = \true)
{
$this->bLenientParsing = $bLenientParsing;
return $this;
}
public function beStrict()
{
return $this->withLenientParsing(\false);
}
}
@@ -0,0 +1,49 @@
<?php
namespace MailPoetVendor\Sabberworm\CSS\Value;
if (!defined('ABSPATH')) exit;
use MailPoetVendor\Sabberworm\CSS\OutputFormat;
use MailPoetVendor\Sabberworm\CSS\Parsing\ParserState;
class CSSFunction extends ValueList
{
protected $sName;
public function __construct($sName, $aArguments, $sSeparator = ',', $iLineNo = 0)
{
if ($aArguments instanceof RuleValueList) {
$sSeparator = $aArguments->getListSeparator();
$aArguments = $aArguments->getListComponents();
}
$this->sName = $sName;
$this->iLineNo = $iLineNo;
parent::__construct($aArguments, $sSeparator, $iLineNo);
}
public static function parse(ParserState $oParserState, $bIgnoreCase = \false)
{
$mResult = $oParserState->parseIdentifier($bIgnoreCase);
$oParserState->consume('(');
$aArguments = Value::parseValue($oParserState, ['=', ' ', ',']);
$mResult = new CSSFunction($mResult, $aArguments, ',', $oParserState->currentLine());
$oParserState->consume(')');
return $mResult;
}
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 OutputFormat());
}
public function render(OutputFormat $oOutputFormat)
{
$aArguments = parent::render($oOutputFormat);
return "{$this->sName}({$aArguments})";
}
}
@@ -0,0 +1,66 @@
<?php
namespace MailPoetVendor\Sabberworm\CSS\Value;
if (!defined('ABSPATH')) exit;
use MailPoetVendor\Sabberworm\CSS\OutputFormat;
use MailPoetVendor\Sabberworm\CSS\Parsing\ParserState;
use MailPoetVendor\Sabberworm\CSS\Parsing\SourceException;
use MailPoetVendor\Sabberworm\CSS\Parsing\UnexpectedEOFException;
use MailPoetVendor\Sabberworm\CSS\Parsing\UnexpectedTokenException;
class CSSString extends PrimitiveValue
{
private $sString;
public function __construct($sString, $iLineNo = 0)
{
$this->sString = $sString;
parent::__construct($iLineNo);
}
public static function parse(ParserState $oParserState)
{
$sBegin = $oParserState->peek();
$sQuote = null;
if ($sBegin === "'") {
$sQuote = "'";
} elseif ($sBegin === '"') {
$sQuote = '"';
}
if ($sQuote !== null) {
$oParserState->consume($sQuote);
}
$sResult = "";
$sContent = null;
if ($sQuote === null) {
// Unquoted strings end in whitespace or with braces, brackets, parentheses
while (!\preg_match('/[\\s{}()<>\\[\\]]/isu', $oParserState->peek())) {
$sResult .= $oParserState->parseCharacter(\false);
}
} else {
while (!$oParserState->comes($sQuote)) {
$sContent = $oParserState->parseCharacter(\false);
if ($sContent === null) {
throw new SourceException("Non-well-formed quoted string {$oParserState->peek(3)}", $oParserState->currentLine());
}
$sResult .= $sContent;
}
$oParserState->consume($sQuote);
}
return new CSSString($sResult, $oParserState->currentLine());
}
public function setString($sString)
{
$this->sString = $sString;
}
public function getString()
{
return $this->sString;
}
public function __toString()
{
return $this->render(new OutputFormat());
}
public function render(OutputFormat $oOutputFormat)
{
$sString = \addslashes($this->sString);
$sString = \str_replace("\n", '\\A', $sString);
return $oOutputFormat->getStringQuotingType() . $sString . $oOutputFormat->getStringQuotingType();
}
}
@@ -0,0 +1,68 @@
<?php
namespace MailPoetVendor\Sabberworm\CSS\Value;
if (!defined('ABSPATH')) exit;
use MailPoetVendor\Sabberworm\CSS\Parsing\ParserState;
use MailPoetVendor\Sabberworm\CSS\Parsing\UnexpectedEOFException;
use MailPoetVendor\Sabberworm\CSS\Parsing\UnexpectedTokenException;
class CalcFunction extends CSSFunction
{
const T_OPERAND = 1;
const T_OPERATOR = 2;
public static function parse(ParserState $oParserState, $bIgnoreCase = \false)
{
$aOperators = ['+', '-', '*', '/'];
$sFunction = $oParserState->parseIdentifier();
if ($oParserState->peek() != '(') {
// Found ; or end of line before an opening bracket
throw new UnexpectedTokenException('(', $oParserState->peek(), 'literal', $oParserState->currentLine());
} elseif (!\in_array($sFunction, ['calc', '-moz-calc', '-webkit-calc'])) {
// Found invalid calc definition. Example calc (...
throw new UnexpectedTokenException('calc', $sFunction, 'literal', $oParserState->currentLine());
}
$oParserState->consume('(');
$oCalcList = new CalcRuleValueList($oParserState->currentLine());
$oList = new RuleValueList(',', $oParserState->currentLine());
$iNestingLevel = 0;
$iLastComponentType = null;
while (!$oParserState->comes(')') || $iNestingLevel > 0) {
if ($oParserState->isEnd() && $iNestingLevel === 0) {
break;
}
$oParserState->consumeWhiteSpace();
if ($oParserState->comes('(')) {
$iNestingLevel++;
$oCalcList->addListComponent($oParserState->consume(1));
$oParserState->consumeWhiteSpace();
continue;
} elseif ($oParserState->comes(')')) {
$iNestingLevel--;
$oCalcList->addListComponent($oParserState->consume(1));
$oParserState->consumeWhiteSpace();
continue;
}
if ($iLastComponentType != CalcFunction::T_OPERAND) {
$oVal = Value::parsePrimitiveValue($oParserState);
$oCalcList->addListComponent($oVal);
$iLastComponentType = CalcFunction::T_OPERAND;
} else {
if (\in_array($oParserState->peek(), $aOperators)) {
if ($oParserState->comes('-') || $oParserState->comes('+')) {
if ($oParserState->peek(1, -1) != ' ' || !($oParserState->comes('- ') || $oParserState->comes('+ '))) {
throw new UnexpectedTokenException(" {$oParserState->peek()} ", $oParserState->peek(1, -1) . $oParserState->peek(2), 'literal', $oParserState->currentLine());
}
}
$oCalcList->addListComponent($oParserState->consume(1));
$iLastComponentType = CalcFunction::T_OPERATOR;
} else {
throw new UnexpectedTokenException(\sprintf('Next token was expected to be an operand of type %s. Instead "%s" was found.', \implode(', ', $aOperators), $oVal), '', 'custom', $oParserState->currentLine());
}
}
$oParserState->consumeWhiteSpace();
}
$oList->addListComponent($oCalcList);
if (!$oParserState->isEnd()) {
$oParserState->consume(')');
}
return new CalcFunction($sFunction, $oList, ',', $oParserState->currentLine());
}
}
@@ -0,0 +1,15 @@
<?php
namespace MailPoetVendor\Sabberworm\CSS\Value;
if (!defined('ABSPATH')) exit;
use MailPoetVendor\Sabberworm\CSS\OutputFormat;
class CalcRuleValueList extends RuleValueList
{
public function __construct($iLineNo = 0)
{
parent::__construct(',', $iLineNo);
}
public function render(OutputFormat $oOutputFormat)
{
return $oOutputFormat->implode(' ', $this->aComponents);
}
}
@@ -0,0 +1,95 @@
<?php
namespace MailPoetVendor\Sabberworm\CSS\Value;
if (!defined('ABSPATH')) exit;
use MailPoetVendor\Sabberworm\CSS\OutputFormat;
use MailPoetVendor\Sabberworm\CSS\Parsing\ParserState;
use MailPoetVendor\Sabberworm\CSS\Parsing\UnexpectedEOFException;
use MailPoetVendor\Sabberworm\CSS\Parsing\UnexpectedTokenException;
class Color extends CSSFunction
{
public function __construct(array $aColor, $iLineNo = 0)
{
parent::__construct(\implode('', \array_keys($aColor)), $aColor, ',', $iLineNo);
}
public static function parse(ParserState $oParserState, $bIgnoreCase = \false)
{
$aColor = [];
if ($oParserState->comes('#')) {
$oParserState->consume('#');
$sValue = $oParserState->parseIdentifier(\false);
if ($oParserState->strlen($sValue) === 3) {
$sValue = $sValue[0] . $sValue[0] . $sValue[1] . $sValue[1] . $sValue[2] . $sValue[2];
} elseif ($oParserState->strlen($sValue) === 4) {
$sValue = $sValue[0] . $sValue[0] . $sValue[1] . $sValue[1] . $sValue[2] . $sValue[2] . $sValue[3] . $sValue[3];
}
if ($oParserState->strlen($sValue) === 8) {
$aColor = ['r' => new Size(\intval($sValue[0] . $sValue[1], 16), null, \true, $oParserState->currentLine()), 'g' => new Size(\intval($sValue[2] . $sValue[3], 16), null, \true, $oParserState->currentLine()), 'b' => new Size(\intval($sValue[4] . $sValue[5], 16), null, \true, $oParserState->currentLine()), 'a' => new Size(\round(self::mapRange(\intval($sValue[6] . $sValue[7], 16), 0, 255, 0, 1), 2), null, \true, $oParserState->currentLine())];
} else {
$aColor = ['r' => new Size(\intval($sValue[0] . $sValue[1], 16), null, \true, $oParserState->currentLine()), 'g' => new Size(\intval($sValue[2] . $sValue[3], 16), null, \true, $oParserState->currentLine()), 'b' => new Size(\intval($sValue[4] . $sValue[5], 16), null, \true, $oParserState->currentLine())];
}
} else {
$sColorMode = $oParserState->parseIdentifier(\true);
$oParserState->consumeWhiteSpace();
$oParserState->consume('(');
$bContainsVar = \false;
$iLength = $oParserState->strlen($sColorMode);
for ($i = 0; $i < $iLength; ++$i) {
$oParserState->consumeWhiteSpace();
if ($oParserState->comes('var')) {
$aColor[$sColorMode[$i]] = CSSFunction::parseIdentifierOrFunction($oParserState);
$bContainsVar = \true;
} else {
$aColor[$sColorMode[$i]] = Size::parse($oParserState, \true);
}
if ($bContainsVar && $oParserState->comes(')')) {
// With a var argument the function can have fewer arguments
break;
}
$oParserState->consumeWhiteSpace();
if ($i < $iLength - 1) {
$oParserState->consume(',');
}
}
$oParserState->consume(')');
if ($bContainsVar) {
return new CSSFunction($sColorMode, \array_values($aColor), ',', $oParserState->currentLine());
}
}
return new Color($aColor, $oParserState->currentLine());
}
private static function mapRange($fVal, $fFromMin, $fFromMax, $fToMin, $fToMax)
{
$fFromRange = $fFromMax - $fFromMin;
$fToRange = $fToMax - $fToMin;
$fMultiplier = $fToRange / $fFromRange;
$fNewVal = $fVal - $fFromMin;
$fNewVal *= $fMultiplier;
return $fNewVal + $fToMin;
}
public function getColor()
{
return $this->aComponents;
}
public function setColor(array $aColor)
{
$this->setName(\implode('', \array_keys($aColor)));
$this->aComponents = $aColor;
}
public function getColorDescription()
{
return $this->getName();
}
public function __toString()
{
return $this->render(new OutputFormat());
}
public function render(OutputFormat $oOutputFormat)
{
// Shorthand RGB color values
if ($oOutputFormat->getRGBHashNotation() && \implode('', \array_keys($this->aComponents)) === 'rgb') {
$sResult = \sprintf('%02x%02x%02x', $this->aComponents['r']->getSize(), $this->aComponents['g']->getSize(), $this->aComponents['b']->getSize());
return '#' . ($sResult[0] == $sResult[1] && $sResult[2] == $sResult[3] && $sResult[4] == $sResult[5] ? "{$sResult[0]}{$sResult[2]}{$sResult[4]}" : $sResult);
}
return parent::render($oOutputFormat);
}
}
@@ -0,0 +1,44 @@
<?php
namespace MailPoetVendor\Sabberworm\CSS\Value;
if (!defined('ABSPATH')) exit;
use MailPoetVendor\Sabberworm\CSS\OutputFormat;
use MailPoetVendor\Sabberworm\CSS\Parsing\ParserState;
use MailPoetVendor\Sabberworm\CSS\Parsing\UnexpectedEOFException;
use MailPoetVendor\Sabberworm\CSS\Parsing\UnexpectedTokenException;
class LineName extends ValueList
{
public function __construct(array $aComponents = [], $iLineNo = 0)
{
parent::__construct($aComponents, ' ', $iLineNo);
}
public static function parse(ParserState $oParserState)
{
$oParserState->consume('[');
$oParserState->consumeWhiteSpace();
$aNames = [];
do {
if ($oParserState->getSettings()->bLenientParsing) {
try {
$aNames[] = $oParserState->parseIdentifier();
} catch (UnexpectedTokenException $e) {
if (!$oParserState->comes(']')) {
throw $e;
}
}
} else {
$aNames[] = $oParserState->parseIdentifier();
}
$oParserState->consumeWhiteSpace();
} while (!$oParserState->comes(']'));
$oParserState->consume(']');
return new LineName($aNames, $oParserState->currentLine());
}
public function __toString()
{
return $this->render(new OutputFormat());
}
public function render(OutputFormat $oOutputFormat)
{
return '[' . parent::render(OutputFormat::createCompact()) . ']';
}
}
@@ -0,0 +1,10 @@
<?php
namespace MailPoetVendor\Sabberworm\CSS\Value;
if (!defined('ABSPATH')) exit;
abstract class PrimitiveValue extends Value
{
public function __construct($iLineNo = 0)
{
parent::__construct($iLineNo);
}
}
@@ -0,0 +1,10 @@
<?php
namespace MailPoetVendor\Sabberworm\CSS\Value;
if (!defined('ABSPATH')) exit;
class RuleValueList extends ValueList
{
public function __construct($sSeparator = ',', $iLineNo = 0)
{
parent::__construct([], $sSeparator, $iLineNo);
}
}
@@ -0,0 +1,121 @@
<?php
namespace MailPoetVendor\Sabberworm\CSS\Value;
if (!defined('ABSPATH')) exit;
use MailPoetVendor\Sabberworm\CSS\OutputFormat;
use MailPoetVendor\Sabberworm\CSS\Parsing\ParserState;
use MailPoetVendor\Sabberworm\CSS\Parsing\UnexpectedEOFException;
use MailPoetVendor\Sabberworm\CSS\Parsing\UnexpectedTokenException;
class Size extends PrimitiveValue
{
const ABSOLUTE_SIZE_UNITS = ['px', 'cm', 'mm', 'mozmm', 'in', 'pt', 'pc', 'vh', 'vw', 'vmin', 'vmax', 'rem'];
const RELATIVE_SIZE_UNITS = ['%', 'em', 'ex', 'ch', 'fr'];
const NON_SIZE_UNITS = ['deg', 'grad', 'rad', 's', 'ms', 'turn', 'Hz', 'kHz'];
private static $SIZE_UNITS = null;
private $fSize;
private $sUnit;
private $bIsColorComponent;
public function __construct($fSize, $sUnit = null, $bIsColorComponent = \false, $iLineNo = 0)
{
parent::__construct($iLineNo);
$this->fSize = (float) $fSize;
$this->sUnit = $sUnit;
$this->bIsColorComponent = $bIsColorComponent;
}
public static function parse(ParserState $oParserState, $bIsColorComponent = \false)
{
$sSize = '';
if ($oParserState->comes('-')) {
$sSize .= $oParserState->consume('-');
}
while (\is_numeric($oParserState->peek()) || $oParserState->comes('.') || $oParserState->comes('e', \true)) {
if ($oParserState->comes('.')) {
$sSize .= $oParserState->consume('.');
} elseif ($oParserState->comes('e', \true)) {
$sLookahead = $oParserState->peek(1, 1);
if (\is_numeric($sLookahead) || $sLookahead === '+' || $sLookahead === '-') {
$sSize .= $oParserState->consume(2);
} else {
break;
// Reached the unit part of the number like "em" or "ex"
}
} else {
$sSize .= $oParserState->consume(1);
}
}
$sUnit = null;
$aSizeUnits = self::getSizeUnits();
foreach ($aSizeUnits as $iLength => &$aValues) {
$sKey = \strtolower($oParserState->peek($iLength));
if (\array_key_exists($sKey, $aValues)) {
if (($sUnit = $aValues[$sKey]) !== null) {
$oParserState->consume($iLength);
break;
}
}
}
return new Size((float) $sSize, $sUnit, $bIsColorComponent, $oParserState->currentLine());
}
private static function getSizeUnits()
{
if (!\is_array(self::$SIZE_UNITS)) {
self::$SIZE_UNITS = [];
foreach (\array_merge(self::ABSOLUTE_SIZE_UNITS, self::RELATIVE_SIZE_UNITS, self::NON_SIZE_UNITS) as $val) {
$iSize = \strlen($val);
if (!isset(self::$SIZE_UNITS[$iSize])) {
self::$SIZE_UNITS[$iSize] = [];
}
self::$SIZE_UNITS[$iSize][\strtolower($val)] = $val;
}
\krsort(self::$SIZE_UNITS, \SORT_NUMERIC);
}
return self::$SIZE_UNITS;
}
public function setUnit($sUnit)
{
$this->sUnit = $sUnit;
}
public function getUnit()
{
return $this->sUnit;
}
public function setSize($fSize)
{
$this->fSize = (float) $fSize;
}
public function getSize()
{
return $this->fSize;
}
public function isColorComponent()
{
return $this->bIsColorComponent;
}
public function isSize()
{
if (\in_array($this->sUnit, self::NON_SIZE_UNITS, \true)) {
return \false;
}
return !$this->isColorComponent();
}
public function isRelative()
{
if (\in_array($this->sUnit, self::RELATIVE_SIZE_UNITS, \true)) {
return \true;
}
if ($this->sUnit === null && $this->fSize != 0) {
return \true;
}
return \false;
}
public function __toString()
{
return $this->render(new OutputFormat());
}
public function render(OutputFormat $oOutputFormat)
{
$l = \localeconv();
$sPoint = \preg_quote($l['decimal_point'], '/');
$sSize = \preg_match("/[\\d\\.]+e[+-]?\\d+/i", (string) $this->fSize) ? \preg_replace("/{$sPoint}?0+\$/", "", \sprintf("%f", $this->fSize)) : $this->fSize;
return \preg_replace(["/{$sPoint}/", "/^(-?)0\\./"], ['.', '$1.'], $sSize) . ($this->sUnit === null ? '' : $this->sUnit);
}
}
@@ -0,0 +1,59 @@
<?php
namespace MailPoetVendor\Sabberworm\CSS\Value;
if (!defined('ABSPATH')) exit;
use MailPoetVendor\Sabberworm\CSS\OutputFormat;
use MailPoetVendor\Sabberworm\CSS\Parsing\ParserState;
use MailPoetVendor\Sabberworm\CSS\Parsing\SourceException;
use MailPoetVendor\Sabberworm\CSS\Parsing\UnexpectedEOFException;
use MailPoetVendor\Sabberworm\CSS\Parsing\UnexpectedTokenException;
class URL extends PrimitiveValue
{
private $oURL;
public function __construct(CSSString $oURL, $iLineNo = 0)
{
parent::__construct($iLineNo);
$this->oURL = $oURL;
}
public static function parse(ParserState $oParserState)
{
$oAnchor = $oParserState->anchor();
$sIdentifier = '';
for ($i = 0; $i < 3; $i++) {
$sChar = $oParserState->parseCharacter(\true);
if ($sChar === null) {
break;
}
$sIdentifier .= $sChar;
}
$bUseUrl = $oParserState->streql($sIdentifier, 'url');
if ($bUseUrl) {
$oParserState->consumeWhiteSpace();
$oParserState->consume('(');
} else {
$oAnchor->backtrack();
}
$oParserState->consumeWhiteSpace();
$oResult = new URL(CSSString::parse($oParserState), $oParserState->currentLine());
if ($bUseUrl) {
$oParserState->consumeWhiteSpace();
$oParserState->consume(')');
}
return $oResult;
}
public function setURL(CSSString $oURL)
{
$this->oURL = $oURL;
}
public function getURL()
{
return $this->oURL;
}
public function __toString()
{
return $this->render(new OutputFormat());
}
public function render(OutputFormat $oOutputFormat)
{
return "url({$this->oURL->render($oOutputFormat)})";
}
}
@@ -0,0 +1,129 @@
<?php
namespace MailPoetVendor\Sabberworm\CSS\Value;
if (!defined('ABSPATH')) exit;
use MailPoetVendor\Sabberworm\CSS\Parsing\ParserState;
use MailPoetVendor\Sabberworm\CSS\Parsing\SourceException;
use MailPoetVendor\Sabberworm\CSS\Parsing\UnexpectedEOFException;
use MailPoetVendor\Sabberworm\CSS\Parsing\UnexpectedTokenException;
use MailPoetVendor\Sabberworm\CSS\Renderable;
abstract class Value implements Renderable
{
protected $iLineNo;
public function __construct($iLineNo = 0)
{
$this->iLineNo = $iLineNo;
}
public static function parseValue(ParserState $oParserState, array $aListDelimiters = [])
{
$aStack = [];
$oParserState->consumeWhiteSpace();
//Build a list of delimiters and parsed values
while (!($oParserState->comes('}') || $oParserState->comes(';') || $oParserState->comes('!') || $oParserState->comes(')') || $oParserState->comes('\\') || $oParserState->isEnd())) {
if (\count($aStack) > 0) {
$bFoundDelimiter = \false;
foreach ($aListDelimiters as $sDelimiter) {
if ($oParserState->comes($sDelimiter)) {
\array_push($aStack, $oParserState->consume($sDelimiter));
$oParserState->consumeWhiteSpace();
$bFoundDelimiter = \true;
break;
}
}
if (!$bFoundDelimiter) {
//Whitespace was the list delimiter
\array_push($aStack, ' ');
}
}
\array_push($aStack, self::parsePrimitiveValue($oParserState));
$oParserState->consumeWhiteSpace();
}
// Convert the list to list objects
foreach ($aListDelimiters as $sDelimiter) {
if (\count($aStack) === 1) {
return $aStack[0];
}
$iStartPosition = null;
while (($iStartPosition = \array_search($sDelimiter, $aStack, \true)) !== \false) {
$iLength = 2;
//Number of elements to be joined
for ($i = $iStartPosition + 2; $i < \count($aStack); $i += 2, ++$iLength) {
if ($sDelimiter !== $aStack[$i]) {
break;
}
}
$oList = new RuleValueList($sDelimiter, $oParserState->currentLine());
for ($i = $iStartPosition - 1; $i - $iStartPosition + 1 < $iLength * 2; $i += 2) {
$oList->addListComponent($aStack[$i]);
}
\array_splice($aStack, $iStartPosition - 1, $iLength * 2 - 1, [$oList]);
}
}
if (!isset($aStack[0])) {
throw new UnexpectedTokenException(" {$oParserState->peek()} ", $oParserState->peek(1, -1) . $oParserState->peek(2), 'literal', $oParserState->currentLine());
}
return $aStack[0];
}
public static function parseIdentifierOrFunction(ParserState $oParserState, $bIgnoreCase = \false)
{
$oAnchor = $oParserState->anchor();
$mResult = $oParserState->parseIdentifier($bIgnoreCase);
if ($oParserState->comes('(')) {
$oAnchor->backtrack();
if ($oParserState->streql('url', $mResult)) {
$mResult = URL::parse($oParserState);
} elseif ($oParserState->streql('calc', $mResult) || $oParserState->streql('-webkit-calc', $mResult) || $oParserState->streql('-moz-calc', $mResult)) {
$mResult = CalcFunction::parse($oParserState);
} else {
$mResult = CSSFunction::parse($oParserState, $bIgnoreCase);
}
}
return $mResult;
}
public static function parsePrimitiveValue(ParserState $oParserState)
{
$oValue = null;
$oParserState->consumeWhiteSpace();
if (\is_numeric($oParserState->peek()) || $oParserState->comes('-.') && \is_numeric($oParserState->peek(1, 2)) || ($oParserState->comes('-') || $oParserState->comes('.')) && \is_numeric($oParserState->peek(1, 1))) {
$oValue = Size::parse($oParserState);
} elseif ($oParserState->comes('#') || $oParserState->comes('rgb', \true) || $oParserState->comes('hsl', \true)) {
$oValue = Color::parse($oParserState);
} elseif ($oParserState->comes("'") || $oParserState->comes('"')) {
$oValue = CSSString::parse($oParserState);
} elseif ($oParserState->comes("progid:") && $oParserState->getSettings()->bLenientParsing) {
$oValue = self::parseMicrosoftFilter($oParserState);
} elseif ($oParserState->comes("[")) {
$oValue = LineName::parse($oParserState);
} elseif ($oParserState->comes("U+")) {
$oValue = self::parseUnicodeRangeValue($oParserState);
} else {
$oValue = self::parseIdentifierOrFunction($oParserState);
}
$oParserState->consumeWhiteSpace();
return $oValue;
}
private static function parseMicrosoftFilter(ParserState $oParserState)
{
$sFunction = $oParserState->consumeUntil('(', \false, \true);
$aArguments = Value::parseValue($oParserState, [',', '=']);
return new CSSFunction($sFunction, $aArguments, ',', $oParserState->currentLine());
}
private static function parseUnicodeRangeValue(ParserState $oParserState)
{
$iCodepointMaxLength = 6;
// Code points outside BMP can use up to six digits
$sRange = "";
$oParserState->consume("U+");
do {
if ($oParserState->comes('-')) {
$iCodepointMaxLength = 13;
// Max length is 2 six digit code points + the dash(-) between them
}
$sRange .= $oParserState->consume(1);
} while (\strlen($sRange) < $iCodepointMaxLength && \preg_match("/[A-Fa-f0-9\\?-]/", $oParserState->peek()));
return "U+{$sRange}";
}
public function getLineNo()
{
return $this->iLineNo;
}
}
@@ -0,0 +1,46 @@
<?php
namespace MailPoetVendor\Sabberworm\CSS\Value;
if (!defined('ABSPATH')) exit;
use MailPoetVendor\Sabberworm\CSS\OutputFormat;
abstract class ValueList extends Value
{
protected $aComponents;
protected $sSeparator;
public function __construct($aComponents = [], $sSeparator = ',', $iLineNo = 0)
{
parent::__construct($iLineNo);
if (!\is_array($aComponents)) {
$aComponents = [$aComponents];
}
$this->aComponents = $aComponents;
$this->sSeparator = $sSeparator;
}
public function addListComponent($mComponent)
{
$this->aComponents[] = $mComponent;
}
public function getListComponents()
{
return $this->aComponents;
}
public function setListComponents(array $aComponents)
{
$this->aComponents = $aComponents;
}
public function getListSeparator()
{
return $this->sSeparator;
}
public function setListSeparator($sSeparator)
{
$this->sSeparator = $sSeparator;
}
public function __toString()
{
return $this->render(new OutputFormat());
}
public function render(OutputFormat $oOutputFormat)
{
return $oOutputFormat->implode($oOutputFormat->spaceBeforeListArgumentSeparator($this->sSeparator) . $this->sSeparator . $oOutputFormat->spaceAfterListArgumentSeparator($this->sSeparator), $this->aComponents);
}
}