Merge branch 'MDL-75475-master' of https://github.com/davewoloszyn/moodle

This commit is contained in:
Sara Arjona 2022-10-20 08:16:26 +02:00
commit 2739a25ffb
40 changed files with 5236 additions and 3066 deletions

View File

@ -2,49 +2,82 @@
namespace Sabberworm\CSS\CSSList;
use Sabberworm\CSS\OutputFormat;
use Sabberworm\CSS\Property\AtRule;
/**
* A BlockList constructed by an unknown @-rule. @media rules are rendered into AtRuleBlockList objects.
* A `BlockList` constructed by an unknown at-rule. `@media` rules are rendered into `AtRuleBlockList` objects.
*/
class AtRuleBlockList extends CSSBlockList implements AtRule {
class AtRuleBlockList extends CSSBlockList implements AtRule
{
/**
* @var string
*/
private $sType;
private $sType;
private $sArgs;
/**
* @var string
*/
private $sArgs;
public function __construct($sType, $sArgs = '', $iLineNo = 0) {
parent::__construct($iLineNo);
$this->sType = $sType;
$this->sArgs = $sArgs;
}
/**
* @param string $sType
* @param string $sArgs
* @param int $iLineNo
*/
public function __construct($sType, $sArgs = '', $iLineNo = 0)
{
parent::__construct($iLineNo);
$this->sType = $sType;
$this->sArgs = $sArgs;
}
public function atRuleName() {
return $this->sType;
}
/**
* @return string
*/
public function atRuleName()
{
return $this->sType;
}
public function atRuleArgs() {
return $this->sArgs;
}
/**
* @return string
*/
public function atRuleArgs()
{
return $this->sArgs;
}
public function __toString() {
return $this->render(new \Sabberworm\CSS\OutputFormat());
}
/**
* @return string
*/
public function __toString()
{
return $this->render(new OutputFormat());
}
public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) {
$sArgs = $this->sArgs;
if($sArgs) {
$sArgs = ' ' . $sArgs;
}
$sResult = $oOutputFormat->sBeforeAtRuleBlock;
$sResult .= "@{$this->sType}$sArgs{$oOutputFormat->spaceBeforeOpeningBrace()}{";
$sResult .= parent::render($oOutputFormat);
$sResult .= '}';
$sResult .= $oOutputFormat->sAfterAtRuleBlock;
return $sResult;
}
/**
* @return string
*/
public function render(OutputFormat $oOutputFormat)
{
$sArgs = $this->sArgs;
if ($sArgs) {
$sArgs = ' ' . $sArgs;
}
$sResult = $oOutputFormat->sBeforeAtRuleBlock;
$sResult .= "@{$this->sType}$sArgs{$oOutputFormat->spaceBeforeOpeningBrace()}{";
$sResult .= parent::render($oOutputFormat);
$sResult .= '}';
$sResult .= $oOutputFormat->sAfterAtRuleBlock;
return $sResult;
}
public function isRootList() {
return false;
}
}
/**
* @return bool
*/
public function isRootList()
{
return false;
}
}

View File

@ -2,106 +2,142 @@
namespace Sabberworm\CSS\CSSList;
use Sabberworm\CSS\RuleSet\DeclarationBlock;
use Sabberworm\CSS\RuleSet\RuleSet;
use Sabberworm\CSS\Property\Selector;
use Sabberworm\CSS\Rule\Rule;
use Sabberworm\CSS\Value\ValueList;
use Sabberworm\CSS\RuleSet\DeclarationBlock;
use Sabberworm\CSS\RuleSet\RuleSet;
use Sabberworm\CSS\Value\CSSFunction;
use Sabberworm\CSS\Value\Value;
use Sabberworm\CSS\Value\ValueList;
/**
* A CSSBlockList is a CSSList whose DeclarationBlocks are guaranteed to contain valid declaration blocks or at-rules.
* Most CSSLists conform to this category but some at-rules (such as @keyframes) do not.
* A `CSSBlockList` is a `CSSList` whose `DeclarationBlock`s are guaranteed to contain valid declaration blocks or
* at-rules.
*
* Most `CSSList`s conform to this category but some at-rules (such as `@keyframes`) do not.
*/
abstract class CSSBlockList extends CSSList {
public function __construct($iLineNo = 0) {
parent::__construct($iLineNo);
}
abstract class CSSBlockList extends CSSList
{
/**
* @param int $iLineNo
*/
public function __construct($iLineNo = 0)
{
parent::__construct($iLineNo);
}
protected function allDeclarationBlocks(&$aResult) {
foreach ($this->aContents as $mContent) {
if ($mContent instanceof DeclarationBlock) {
$aResult[] = $mContent;
} else if ($mContent instanceof CSSBlockList) {
$mContent->allDeclarationBlocks($aResult);
}
}
}
/**
* @param array<int, DeclarationBlock> $aResult
*
* @return void
*/
protected function allDeclarationBlocks(array &$aResult)
{
foreach ($this->aContents as $mContent) {
if ($mContent instanceof DeclarationBlock) {
$aResult[] = $mContent;
} elseif ($mContent instanceof CSSBlockList) {
$mContent->allDeclarationBlocks($aResult);
}
}
}
protected function allRuleSets(&$aResult) {
foreach ($this->aContents as $mContent) {
if ($mContent instanceof RuleSet) {
$aResult[] = $mContent;
} else if ($mContent instanceof CSSBlockList) {
$mContent->allRuleSets($aResult);
}
}
}
/**
* @param array<int, RuleSet> $aResult
*
* @return void
*/
protected function allRuleSets(array &$aResult)
{
foreach ($this->aContents as $mContent) {
if ($mContent instanceof RuleSet) {
$aResult[] = $mContent;
} elseif ($mContent instanceof CSSBlockList) {
$mContent->allRuleSets($aResult);
}
}
}
protected function allValues($oElement, &$aResult, $sSearchString = null, $bSearchInFunctionArguments = false) {
if ($oElement instanceof CSSBlockList) {
foreach ($oElement->getContents() as $oContent) {
$this->allValues($oContent, $aResult, $sSearchString, $bSearchInFunctionArguments);
}
} else if ($oElement instanceof RuleSet) {
foreach ($oElement->getRules($sSearchString) as $oRule) {
$this->allValues($oRule, $aResult, $sSearchString, $bSearchInFunctionArguments);
}
} else if ($oElement instanceof Rule) {
$this->allValues($oElement->getValue(), $aResult, $sSearchString, $bSearchInFunctionArguments);
} else if ($oElement instanceof ValueList) {
if ($bSearchInFunctionArguments || !($oElement instanceof CSSFunction)) {
foreach ($oElement->getListComponents() as $mComponent) {
$this->allValues($mComponent, $aResult, $sSearchString, $bSearchInFunctionArguments);
}
}
} else {
//Non-List Value or CSSString (CSS identifier)
$aResult[] = $oElement;
}
}
protected function allSelectors(&$aResult, $sSpecificitySearch = null) {
$aDeclarationBlocks = array();
$this->allDeclarationBlocks($aDeclarationBlocks);
foreach ($aDeclarationBlocks as $oBlock) {
foreach ($oBlock->getSelectors() as $oSelector) {
if ($sSpecificitySearch === null) {
$aResult[] = $oSelector;
} else {
$sComparator = '===';
$aSpecificitySearch = explode(' ', $sSpecificitySearch);
$iTargetSpecificity = $aSpecificitySearch[0];
if(count($aSpecificitySearch) > 1) {
$sComparator = $aSpecificitySearch[0];
$iTargetSpecificity = $aSpecificitySearch[1];
}
$iTargetSpecificity = (int)$iTargetSpecificity;
$iSelectorSpecificity = $oSelector->getSpecificity();
$bMatches = false;
switch($sComparator) {
case '<=':
$bMatches = $iSelectorSpecificity <= $iTargetSpecificity;
break;
case '<':
$bMatches = $iSelectorSpecificity < $iTargetSpecificity;
break;
case '>=':
$bMatches = $iSelectorSpecificity >= $iTargetSpecificity;
break;
case '>':
$bMatches = $iSelectorSpecificity > $iTargetSpecificity;
break;
default:
$bMatches = $iSelectorSpecificity === $iTargetSpecificity;
break;
}
if ($bMatches) {
$aResult[] = $oSelector;
}
}
}
}
}
/**
* @param CSSList|Rule|RuleSet|Value $oElement
* @param array<int, Value> $aResult
* @param string|null $sSearchString
* @param bool $bSearchInFunctionArguments
*
* @return void
*/
protected function allValues($oElement, array &$aResult, $sSearchString = null, $bSearchInFunctionArguments = false)
{
if ($oElement instanceof CSSBlockList) {
foreach ($oElement->getContents() as $oContent) {
$this->allValues($oContent, $aResult, $sSearchString, $bSearchInFunctionArguments);
}
} elseif ($oElement instanceof RuleSet) {
foreach ($oElement->getRules($sSearchString) as $oRule) {
$this->allValues($oRule, $aResult, $sSearchString, $bSearchInFunctionArguments);
}
} elseif ($oElement instanceof Rule) {
$this->allValues($oElement->getValue(), $aResult, $sSearchString, $bSearchInFunctionArguments);
} elseif ($oElement instanceof ValueList) {
if ($bSearchInFunctionArguments || !($oElement instanceof CSSFunction)) {
foreach ($oElement->getListComponents() as $mComponent) {
$this->allValues($mComponent, $aResult, $sSearchString, $bSearchInFunctionArguments);
}
}
} else {
// Non-List `Value` or `CSSString` (CSS identifier)
$aResult[] = $oElement;
}
}
/**
* @param array<int, Selector> $aResult
* @param string|null $sSpecificitySearch
*
* @return void
*/
protected function allSelectors(array &$aResult, $sSpecificitySearch = null)
{
/** @var array<int, DeclarationBlock> $aDeclarationBlocks */
$aDeclarationBlocks = [];
$this->allDeclarationBlocks($aDeclarationBlocks);
foreach ($aDeclarationBlocks as $oBlock) {
foreach ($oBlock->getSelectors() as $oSelector) {
if ($sSpecificitySearch === null) {
$aResult[] = $oSelector;
} else {
$sComparator = '===';
$aSpecificitySearch = explode(' ', $sSpecificitySearch);
$iTargetSpecificity = $aSpecificitySearch[0];
if (count($aSpecificitySearch) > 1) {
$sComparator = $aSpecificitySearch[0];
$iTargetSpecificity = $aSpecificitySearch[1];
}
$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;
}
}
}
}
}
}

View File

@ -2,9 +2,12 @@
namespace Sabberworm\CSS\CSSList;
use Sabberworm\CSS\Comment\Comment;
use Sabberworm\CSS\Comment\Commentable;
use Sabberworm\CSS\OutputFormat;
use Sabberworm\CSS\Parsing\ParserState;
use Sabberworm\CSS\Parsing\SourceException;
use Sabberworm\CSS\Parsing\UnexpectedEOFException;
use Sabberworm\CSS\Parsing\UnexpectedTokenException;
use Sabberworm\CSS\Property\AtRule;
use Sabberworm\CSS\Property\Charset;
@ -15,201 +18,269 @@ use Sabberworm\CSS\Renderable;
use Sabberworm\CSS\RuleSet\AtRuleSet;
use Sabberworm\CSS\RuleSet\DeclarationBlock;
use Sabberworm\CSS\RuleSet\RuleSet;
use Sabberworm\CSS\Settings;
use Sabberworm\CSS\Value\CSSString;
use Sabberworm\CSS\Value\URL;
use Sabberworm\CSS\Value\Value;
/**
* A CSSList is the most generic container available. Its contents include RuleSet as well as other CSSList objects.
* Also, it may contain Import and Charset objects stemming from @-rules.
* A `CSSList` is the most generic container available. Its contents include `RuleSet` as well as other `CSSList`
* objects.
*
* Also, it may contain `Import` and `Charset` objects stemming from at-rules.
*/
abstract class CSSList implements Renderable, Commentable {
abstract class CSSList implements Renderable, Commentable
{
/**
* @var array<array-key, Comment>
*/
protected $aComments;
protected $aComments;
protected $aContents;
protected $iLineNo;
/**
* @var array<int, RuleSet|CSSList|Import|Charset>
*/
protected $aContents;
public function __construct($iLineNo = 0) {
$this->aComments = array();
$this->aContents = array();
$this->iLineNo = $iLineNo;
}
/**
* @var int
*/
protected $iLineNo;
public static function parseList(ParserState $oParserState, CSSList $oList) {
$bIsRoot = $oList instanceof Document;
if(is_string($oParserState)) {
$oParserState = new ParserState($oParserState);
}
$bLenientParsing = $oParserState->getSettings()->bLenientParsing;
while(!$oParserState->isEnd()) {
$comments = $oParserState->consumeWhiteSpace();
$oListItem = null;
if($bLenientParsing) {
try {
$oListItem = self::parseListItem($oParserState, $oList);
} catch (UnexpectedTokenException $e) {
$oListItem = false;
}
} else {
$oListItem = self::parseListItem($oParserState, $oList);
}
if($oListItem === null) {
// List parsing finished
return;
}
if($oListItem) {
$oListItem->setComments($comments);
$oList->append($oListItem);
}
}
if(!$bIsRoot && !$bLenientParsing) {
throw new SourceException("Unexpected end of document", $oParserState->currentLine());
}
}
/**
* @param int $iLineNo
*/
public function __construct($iLineNo = 0)
{
$this->aComments = [];
$this->aContents = [];
$this->iLineNo = $iLineNo;
}
private static function parseListItem(ParserState $oParserState, CSSList $oList) {
$bIsRoot = $oList instanceof Document;
if ($oParserState->comes('@')) {
$oAtRule = self::parseAtRule($oParserState);
if($oAtRule instanceof Charset) {
if(!$bIsRoot) {
throw new UnexpectedTokenException('@charset may only occur in root document', '', 'custom', $oParserState->currentLine());
}
if(count($oList->getContents()) > 0) {
throw new UnexpectedTokenException('@charset must be the first parseable token in a document', '', 'custom', $oParserState->currentLine());
}
$oParserState->setCharset($oAtRule->getCharset()->getString());
}
return $oAtRule;
} else if ($oParserState->comes('}')) {
$oParserState->consume('}');
if ($bIsRoot) {
if ($oParserState->getSettings()->bLenientParsing) {
while ($oParserState->comes('}')) $oParserState->consume('}');
return DeclarationBlock::parse($oParserState);
} else {
throw new SourceException("Unopened {", $oParserState->currentLine());
}
} else {
return null;
}
} else {
return DeclarationBlock::parse($oParserState);
}
}
/**
* @return void
*
* @throws UnexpectedTokenException
* @throws SourceException
*/
public static function parseList(ParserState $oParserState, CSSList $oList)
{
$bIsRoot = $oList instanceof Document;
if (is_string($oParserState)) {
$oParserState = new ParserState($oParserState, Settings::create());
}
$bLenientParsing = $oParserState->getSettings()->bLenientParsing;
while (!$oParserState->isEnd()) {
$comments = $oParserState->consumeWhiteSpace();
$oListItem = null;
if ($bLenientParsing) {
try {
$oListItem = self::parseListItem($oParserState, $oList);
} catch (UnexpectedTokenException $e) {
$oListItem = false;
}
} else {
$oListItem = self::parseListItem($oParserState, $oList);
}
if ($oListItem === null) {
// List parsing finished
return;
}
if ($oListItem) {
$oListItem->setComments($comments);
$oList->append($oListItem);
}
}
if (!$bIsRoot && !$bLenientParsing) {
throw new SourceException("Unexpected end of document", $oParserState->currentLine());
}
}
private static function parseAtRule(ParserState $oParserState) {
$oParserState->consume('@');
$sIdentifier = $oParserState->parseIdentifier();
$iIdentifierLineNum = $oParserState->currentLine();
$oParserState->consumeWhiteSpace();
if ($sIdentifier === 'import') {
$oLocation = URL::parse($oParserState);
$oParserState->consumeWhiteSpace();
$sMediaQuery = null;
if (!$oParserState->comes(';')) {
$sMediaQuery = $oParserState->consumeUntil(';');
}
$oParserState->consume(';');
return new Import($oLocation, $sMediaQuery, $iIdentifierLineNum);
} else if ($sIdentifier === 'charset') {
$sCharset = CSSString::parse($oParserState);
$oParserState->consumeWhiteSpace();
$oParserState->consume(';');
return new Charset($sCharset, $iIdentifierLineNum);
} else if (self::identifierIs($sIdentifier, 'keyframes')) {
$oResult = new KeyFrame($iIdentifierLineNum);
$oResult->setVendorKeyFrame($sIdentifier);
$oResult->setAnimationName(trim($oParserState->consumeUntil('{', false, true)));
CSSList::parseList($oParserState, $oResult);
return $oResult;
} else if ($sIdentifier === 'namespace') {
$sPrefix = null;
$mUrl = Value::parsePrimitiveValue($oParserState);
if (!$oParserState->comes(';')) {
$sPrefix = $mUrl;
$mUrl = Value::parsePrimitiveValue($oParserState);
}
$oParserState->consume(';');
if ($sPrefix !== null && !is_string($sPrefix)) {
throw new UnexpectedTokenException('Wrong namespace prefix', $sPrefix, 'custom', $iIdentifierLineNum);
}
if (!($mUrl instanceof CSSString || $mUrl instanceof URL)) {
throw new UnexpectedTokenException('Wrong namespace url of invalid type', $mUrl, 'custom', $iIdentifierLineNum);
}
return new CSSNamespace($mUrl, $sPrefix, $iIdentifierLineNum);
} else {
//Unknown other at rule (font-face or such)
$sArgs = trim($oParserState->consumeUntil('{', false, true));
if (substr_count($sArgs, "(") != substr_count($sArgs, ")")) {
if($oParserState->getSettings()->bLenientParsing) {
return NULL;
} else {
throw new SourceException("Unmatched brace count in media query", $oParserState->currentLine());
}
}
$bUseRuleSet = true;
foreach(explode('/', AtRule::BLOCK_RULES) as $sBlockRuleName) {
if(self::identifierIs($sIdentifier, $sBlockRuleName)) {
$bUseRuleSet = false;
break;
}
}
if($bUseRuleSet) {
$oAtRule = new AtRuleSet($sIdentifier, $sArgs, $iIdentifierLineNum);
RuleSet::parseRuleSet($oParserState, $oAtRule);
} else {
$oAtRule = new AtRuleBlockList($sIdentifier, $sArgs, $iIdentifierLineNum);
CSSList::parseList($oParserState, $oAtRule);
}
return $oAtRule;
}
}
/**
* @return AtRuleBlockList|KeyFrame|Charset|CSSNamespace|Import|AtRuleSet|DeclarationBlock|null|false
*
* @throws SourceException
* @throws UnexpectedEOFException
* @throws UnexpectedTokenException
*/
private static function parseListItem(ParserState $oParserState, CSSList $oList)
{
$bIsRoot = $oList instanceof Document;
if ($oParserState->comes('@')) {
$oAtRule = self::parseAtRule($oParserState);
if ($oAtRule instanceof Charset) {
if (!$bIsRoot) {
throw new UnexpectedTokenException(
'@charset may only occur in root document',
'',
'custom',
$oParserState->currentLine()
);
}
if (count($oList->getContents()) > 0) {
throw new UnexpectedTokenException(
'@charset must be the first parseable token in a document',
'',
'custom',
$oParserState->currentLine()
);
}
$oParserState->setCharset($oAtRule->getCharset()->getString());
}
return $oAtRule;
} elseif ($oParserState->comes('}')) {
if (!$oParserState->getSettings()->bLenientParsing) {
throw new UnexpectedTokenException('CSS selector', '}', 'identifier', $oParserState->currentLine());
} else {
if ($bIsRoot) {
if ($oParserState->getSettings()->bLenientParsing) {
return DeclarationBlock::parse($oParserState);
} else {
throw new SourceException("Unopened {", $oParserState->currentLine());
}
} else {
return null;
}
}
} else {
return DeclarationBlock::parse($oParserState, $oList);
}
}
/**
* Tests an identifier for a given value. Since identifiers are all keywords, they can be vendor-prefixed. We need to check for these versions too.
*/
private static function identifierIs($sIdentifier, $sMatch) {
return (strcasecmp($sIdentifier, $sMatch) === 0)
?: preg_match("/^(-\\w+-)?$sMatch$/i", $sIdentifier) === 1;
}
/**
* @param ParserState $oParserState
*
* @return AtRuleBlockList|KeyFrame|Charset|CSSNamespace|Import|AtRuleSet|null
*
* @throws SourceException
* @throws UnexpectedTokenException
* @throws UnexpectedEOFException
*/
private static function parseAtRule(ParserState $oParserState)
{
$oParserState->consume('@');
$sIdentifier = $oParserState->parseIdentifier();
$iIdentifierLineNum = $oParserState->currentLine();
$oParserState->consumeWhiteSpace();
if ($sIdentifier === 'import') {
$oLocation = URL::parse($oParserState);
$oParserState->consumeWhiteSpace();
$sMediaQuery = null;
if (!$oParserState->comes(';')) {
$sMediaQuery = trim($oParserState->consumeUntil([';', ParserState::EOF]));
}
$oParserState->consumeUntil([';', ParserState::EOF], true, true);
return new Import($oLocation, $sMediaQuery ?: null, $iIdentifierLineNum);
} elseif ($sIdentifier === 'charset') {
$sCharset = CSSString::parse($oParserState);
$oParserState->consumeWhiteSpace();
$oParserState->consumeUntil([';', ParserState::EOF], true, true);
return new Charset($sCharset, $iIdentifierLineNum);
} elseif (self::identifierIs($sIdentifier, 'keyframes')) {
$oResult = new KeyFrame($iIdentifierLineNum);
$oResult->setVendorKeyFrame($sIdentifier);
$oResult->setAnimationName(trim($oParserState->consumeUntil('{', false, true)));
CSSList::parseList($oParserState, $oResult);
if ($oParserState->comes('}')) {
$oParserState->consume('}');
}
return $oResult;
} elseif ($sIdentifier === 'namespace') {
$sPrefix = null;
$mUrl = Value::parsePrimitiveValue($oParserState);
if (!$oParserState->comes(';')) {
$sPrefix = $mUrl;
$mUrl = Value::parsePrimitiveValue($oParserState);
}
$oParserState->consumeUntil([';', ParserState::EOF], true, true);
if ($sPrefix !== null && !is_string($sPrefix)) {
throw new UnexpectedTokenException('Wrong namespace prefix', $sPrefix, 'custom', $iIdentifierLineNum);
}
if (!($mUrl instanceof CSSString || $mUrl instanceof URL)) {
throw new UnexpectedTokenException(
'Wrong namespace url of invalid type',
$mUrl,
'custom',
$iIdentifierLineNum
);
}
return new CSSNamespace($mUrl, $sPrefix, $iIdentifierLineNum);
} else {
// Unknown other at rule (font-face or such)
$sArgs = trim($oParserState->consumeUntil('{', false, true));
if (substr_count($sArgs, "(") != substr_count($sArgs, ")")) {
if ($oParserState->getSettings()->bLenientParsing) {
return null;
} else {
throw new SourceException("Unmatched brace count in media query", $oParserState->currentLine());
}
}
$bUseRuleSet = true;
foreach (explode('/', AtRule::BLOCK_RULES) as $sBlockRuleName) {
if (self::identifierIs($sIdentifier, $sBlockRuleName)) {
$bUseRuleSet = false;
break;
}
}
if ($bUseRuleSet) {
$oAtRule = new AtRuleSet($sIdentifier, $sArgs, $iIdentifierLineNum);
RuleSet::parseRuleSet($oParserState, $oAtRule);
} else {
$oAtRule = new AtRuleBlockList($sIdentifier, $sArgs, $iIdentifierLineNum);
CSSList::parseList($oParserState, $oAtRule);
if ($oParserState->comes('}')) {
$oParserState->consume('}');
}
}
return $oAtRule;
}
}
/**
* Tests an identifier for a given value. Since identifiers are all keywords, they can be vendor-prefixed.
* We need to check for these versions too.
*
* @param string $sIdentifier
* @param string $sMatch
*
* @return bool
*/
private static function identifierIs($sIdentifier, $sMatch)
{
return (strcasecmp($sIdentifier, $sMatch) === 0)
?: preg_match("/^(-\\w+-)?$sMatch$/i", $sIdentifier) === 1;
}
/**
* @return int
*/
public function getLineNo() {
return $this->iLineNo;
}
/**
* @return int
*/
public function getLineNo()
{
return $this->iLineNo;
}
/**
* Prepend item to list of contents.
*
* @param object $oItem Item.
*/
public function prepend($oItem) {
array_unshift($this->aContents, $oItem);
}
/**
* Prepends an item to the list of contents.
*
* @param RuleSet|CSSList|Import|Charset $oItem
*
* @return void
*/
public function prepend($oItem)
{
array_unshift($this->aContents, $oItem);
}
/**
* Append item to list of contents.
*
* @param object $oItem Item.
*/
public function append($oItem) {
$this->aContents[] = $oItem;
}
/**
* Splice the list of contents.
*
* @param int $iOffset Offset.
* @param int $iLength Length. Optional.
* @param RuleSet[] $mReplacement Replacement. Optional.
*/
public function splice($iOffset, $iLength = null, $mReplacement = null) {
array_splice($this->aContents, $iOffset, $iLength, $mReplacement);
}
/**
* Appends an item to tje list of contents.
*
* @param RuleSet|CSSList|Import|Charset $oItem
*
* @return void
*/
public function append($oItem)
{
$this->aContents[] = $oItem;
}
/**
* Insert an item before its sibling.
@ -225,137 +296,197 @@ abstract class CSSList implements Renderable, Commentable {
array_splice($this->aContents, $iIndex, 0, array($oItem));
}
/**
* Removes an item from the CSS list.
* @param RuleSet|Import|Charset|CSSList $oItemToRemove May be a RuleSet (most likely a DeclarationBlock), a Import, a Charset or another CSSList (most likely a MediaQuery)
* @return bool Whether the item was removed.
*/
public function remove($oItemToRemove) {
$iKey = array_search($oItemToRemove, $this->aContents, true);
if ($iKey !== false) {
unset($this->aContents[$iKey]);
return true;
}
return false;
}
/**
* Splices the list of contents.
*
* @param int $iOffset
* @param int $iLength
* @param array<int, RuleSet|CSSList|Import|Charset> $mReplacement
*
* @return void
*/
public function splice($iOffset, $iLength = null, $mReplacement = null)
{
array_splice($this->aContents, $iOffset, $iLength, $mReplacement);
}
/**
* Replaces an item from the CSS list.
* @param RuleSet|Import|Charset|CSSList $oItemToRemove May be a RuleSet (most likely a DeclarationBlock), a Import, a Charset or another CSSList (most likely a MediaQuery)
*/
public function replace($oOldItem, $oNewItem) {
$iKey = array_search($oOldItem, $this->aContents, true);
if ($iKey !== false) {
array_splice($this->aContents, $iKey, 1, $oNewItem);
return true;
}
return false;
}
/**
* Removes an item from the CSS list.
*
* @param RuleSet|Import|Charset|CSSList $oItemToRemove
* May be a RuleSet (most likely a DeclarationBlock), a Import,
* a Charset or another CSSList (most likely a MediaQuery)
*
* @return bool whether the item was removed
*/
public function remove($oItemToRemove)
{
$iKey = array_search($oItemToRemove, $this->aContents, true);
if ($iKey !== false) {
unset($this->aContents[$iKey]);
return true;
}
return false;
}
/**
* Set the contents.
* @param array $aContents Objects to set as content.
*/
public function setContents(array $aContents) {
$this->aContents = array();
foreach ($aContents as $content) {
$this->append($content);
}
}
/**
* Replaces an item from the CSS list.
*
* @param RuleSet|Import|Charset|CSSList $oOldItem
* May be a `RuleSet` (most likely a `DeclarationBlock`), an `Import`, a `Charset`
* or another `CSSList` (most likely a `MediaQuery`)
*
* @return bool
*/
public function replace($oOldItem, $mNewItem)
{
$iKey = array_search($oOldItem, $this->aContents, true);
if ($iKey !== false) {
if (is_array($mNewItem)) {
array_splice($this->aContents, $iKey, 1, $mNewItem);
} else {
array_splice($this->aContents, $iKey, 1, [$mNewItem]);
}
return true;
}
return false;
}
/**
* Removes a declaration block from the CSS list if it matches all given selectors.
* @param array|string $mSelector The selectors to match.
* @param boolean $bRemoveAll Whether to stop at the first declaration block found or remove all blocks
*/
public function removeDeclarationBlockBySelector($mSelector, $bRemoveAll = false) {
if ($mSelector instanceof DeclarationBlock) {
$mSelector = $mSelector->getSelectors();
}
if (!is_array($mSelector)) {
$mSelector = explode(',', $mSelector);
}
foreach ($mSelector as $iKey => &$mSel) {
if (!($mSel instanceof Selector)) {
$mSel = new Selector($mSel);
}
}
foreach ($this->aContents as $iKey => $mItem) {
if (!($mItem instanceof DeclarationBlock)) {
continue;
}
if ($mItem->getSelectors() == $mSelector) {
unset($this->aContents[$iKey]);
if (!$bRemoveAll) {
return;
}
}
}
}
/**
* @param array<int, RuleSet|Import|Charset|CSSList> $aContents
*/
public function setContents(array $aContents)
{
$this->aContents = [];
foreach ($aContents as $content) {
$this->append($content);
}
}
public function __toString() {
return $this->render(new \Sabberworm\CSS\OutputFormat());
}
/**
* Removes a declaration block from the CSS list if it matches all given selectors.
*
* @param DeclarationBlock|array<array-key, Selector>|string $mSelector the selectors to match
* @param bool $bRemoveAll whether to stop at the first declaration block found or remove all blocks
*
* @return void
*/
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 render(\Sabberworm\CSS\OutputFormat $oOutputFormat) {
$sResult = '';
$bIsFirst = true;
$oNextLevel = $oOutputFormat;
if(!$this->isRootList()) {
$oNextLevel = $oOutputFormat->nextLevel();
}
foreach ($this->aContents as $oContent) {
$sRendered = $oOutputFormat->safely(function() use ($oNextLevel, $oContent) {
return $oContent->render($oNextLevel);
});
if($sRendered === null) {
continue;
}
if($bIsFirst) {
$bIsFirst = false;
$sResult .= $oNextLevel->spaceBeforeBlocks();
} else {
$sResult .= $oNextLevel->spaceBetweenBlocks();
}
$sResult .= $sRendered;
}
/**
* @return string
*/
public function __toString()
{
return $this->render(new OutputFormat());
}
if(!$bIsFirst) {
// Had some output
$sResult .= $oOutputFormat->spaceAfterBlocks();
}
/**
* @return string
*/
public function render(OutputFormat $oOutputFormat)
{
$sResult = '';
$bIsFirst = true;
$oNextLevel = $oOutputFormat;
if (!$this->isRootList()) {
$oNextLevel = $oOutputFormat->nextLevel();
}
foreach ($this->aContents as $oContent) {
$sRendered = $oOutputFormat->safely(function () use ($oNextLevel, $oContent) {
return $oContent->render($oNextLevel);
});
if ($sRendered === null) {
continue;
}
if ($bIsFirst) {
$bIsFirst = false;
$sResult .= $oNextLevel->spaceBeforeBlocks();
} else {
$sResult .= $oNextLevel->spaceBetweenBlocks();
}
$sResult .= $sRendered;
}
return $sResult;
}
if (!$bIsFirst) {
// Had some output
$sResult .= $oOutputFormat->spaceAfterBlocks();
}
/**
* Return true if the list can not be further outdented. Only important when rendering.
*/
public abstract function isRootList();
return $sResult;
}
public function getContents() {
return $this->aContents;
}
/**
* Return true if the list can not be further outdented. Only important when rendering.
*
* @return bool
*/
abstract public function isRootList();
/**
* @param array $aComments Array of comments.
*/
public function addComments(array $aComments) {
$this->aComments = array_merge($this->aComments, $aComments);
}
/**
* @return array<int, RuleSet|Import|Charset|CSSList>
*/
public function getContents()
{
return $this->aContents;
}
/**
* @return array
*/
public function getComments() {
return $this->aComments;
}
/**
* @param array<array-key, Comment> $aComments
*
* @return void
*/
public function addComments(array $aComments)
{
$this->aComments = array_merge($this->aComments, $aComments);
}
/**
* @param array $aComments Array containing Comment objects.
*/
public function setComments(array $aComments) {
$this->aComments = $aComments;
}
/**
* @return array<array-key, Comment>
*/
public function getComments()
{
return $this->aComments;
}
/**
* @param array<array-key, Comment> $aComments
*
* @return void
*/
public function setComments(array $aComments)
{
$this->aComments = $aComments;
}
}

View File

@ -2,109 +2,171 @@
namespace Sabberworm\CSS\CSSList;
use Sabberworm\CSS\OutputFormat;
use Sabberworm\CSS\Parsing\ParserState;
use Sabberworm\CSS\Parsing\SourceException;
use Sabberworm\CSS\Property\Selector;
use Sabberworm\CSS\RuleSet\DeclarationBlock;
use Sabberworm\CSS\RuleSet\RuleSet;
use Sabberworm\CSS\Value\Value;
/**
* The root CSSList of a parsed file. Contains all top-level css contents, mostly declaration blocks, but also any @-rules encountered.
* The root `CSSList` of a parsed file. Contains all top-level CSS contents, mostly declaration blocks,
* but also any at-rules encountered.
*/
class Document extends CSSBlockList {
/**
* Document constructor.
* @param int $iLineNo
*/
public function __construct($iLineNo = 0) {
parent::__construct($iLineNo);
}
class Document extends CSSBlockList
{
/**
* @param int $iLineNo
*/
public function __construct($iLineNo = 0)
{
parent::__construct($iLineNo);
}
public static function parse(ParserState $oParserState) {
$oDocument = new Document($oParserState->currentLine());
CSSList::parseList($oParserState, $oDocument);
return $oDocument;
}
/**
* @return Document
*
* @throws SourceException
*/
public static function parse(ParserState $oParserState)
{
$oDocument = new Document($oParserState->currentLine());
CSSList::parseList($oParserState, $oDocument);
return $oDocument;
}
/**
* Gets all DeclarationBlock objects recursively.
*/
public function getAllDeclarationBlocks() {
$aResult = array();
$this->allDeclarationBlocks($aResult);
return $aResult;
}
/**
* Gets all `DeclarationBlock` objects recursively.
*
* @return array<int, DeclarationBlock>
*/
public function getAllDeclarationBlocks()
{
/** @var array<int, DeclarationBlock> $aResult */
$aResult = [];
$this->allDeclarationBlocks($aResult);
return $aResult;
}
/**
* @deprecated use getAllDeclarationBlocks()
*/
public function getAllSelectors() {
return $this->getAllDeclarationBlocks();
}
/**
* Gets all `DeclarationBlock` objects recursively.
*
* @return array<int, DeclarationBlock>
*
* @deprecated will be removed in version 9.0; use `getAllDeclarationBlocks()` instead
*/
public function getAllSelectors()
{
return $this->getAllDeclarationBlocks();
}
/**
* Returns all RuleSet objects found recursively in the tree.
*/
public function getAllRuleSets() {
$aResult = array();
$this->allRuleSets($aResult);
return $aResult;
}
/**
* Returns all `RuleSet` objects found recursively in the tree.
*
* @return array<int, RuleSet>
*/
public function getAllRuleSets()
{
/** @var array<int, RuleSet> $aResult */
$aResult = [];
$this->allRuleSets($aResult);
return $aResult;
}
/**
* Returns all Value objects found recursively in the tree.
* @param (object|string) $mElement the CSSList or RuleSet to start the search from (defaults to the whole document). If a string is given, it is used as rule name filter (@see{RuleSet->getRules()}).
* @param (bool) $bSearchInFunctionArguments whether to also return Value objects used as Function arguments.
*/
public function getAllValues($mElement = null, $bSearchInFunctionArguments = false) {
$sSearchString = null;
if ($mElement === null) {
$mElement = $this;
} else if (is_string($mElement)) {
$sSearchString = $mElement;
$mElement = $this;
}
$aResult = array();
$this->allValues($mElement, $aResult, $sSearchString, $bSearchInFunctionArguments);
return $aResult;
}
/**
* Returns all `Value` objects found recursively in the tree.
*
* @param CSSList|RuleSet|string $mElement
* the `CSSList` or `RuleSet` to start the search from (defaults to the whole document).
* If a string is given, it is used as rule name filter.
* @param bool $bSearchInFunctionArguments whether to also return Value objects used as Function arguments.
*
* @return array<int, Value>
*
* @see RuleSet->getRules()
*/
public function getAllValues($mElement = null, $bSearchInFunctionArguments = false)
{
$sSearchString = null;
if ($mElement === null) {
$mElement = $this;
} elseif (is_string($mElement)) {
$sSearchString = $mElement;
$mElement = $this;
}
/** @var array<int, Value> $aResult */
$aResult = [];
$this->allValues($mElement, $aResult, $sSearchString, $bSearchInFunctionArguments);
return $aResult;
}
/**
* Returns all Selector objects found recursively in the tree.
* Note that this does not yield the full DeclarationBlock that the selector belongs to (and, currently, there is no way to get to that).
* @param $sSpecificitySearch An optional filter by specificity. May contain a comparison operator and a number or just a number (defaults to "==").
* @example getSelectorsBySpecificity('>= 100')
*/
public function getSelectorsBySpecificity($sSpecificitySearch = null) {
$aResult = array();
$this->allSelectors($aResult, $sSpecificitySearch);
return $aResult;
}
/**
* Returns all `Selector` objects found recursively in the tree.
*
* Note that this does not yield the full `DeclarationBlock` that the selector belongs to
* (and, currently, there is no way to get to that).
*
* @param string|null $sSpecificitySearch
* An optional filter by specificity.
* May contain a comparison operator and a number or just a number (defaults to "==").
*
* @return array<int, Selector>
* @example `getSelectorsBySpecificity('>= 100')`
*
*/
public function getSelectorsBySpecificity($sSpecificitySearch = null)
{
/** @var array<int, Selector> $aResult */
$aResult = [];
$this->allSelectors($aResult, $sSpecificitySearch);
return $aResult;
}
/**
* Expands all shorthand properties to their long value
*/
public function expandShorthands() {
foreach ($this->getAllDeclarationBlocks() as $oDeclaration) {
$oDeclaration->expandShorthands();
}
}
/**
* Expands all shorthand properties to their long value.
*
* @return void
*/
public function expandShorthands()
{
foreach ($this->getAllDeclarationBlocks() as $oDeclaration) {
$oDeclaration->expandShorthands();
}
}
/**
* Create shorthands properties whenever possible
*/
public function createShorthands() {
foreach ($this->getAllDeclarationBlocks() as $oDeclaration) {
$oDeclaration->createShorthands();
}
}
/**
* Create shorthands properties whenever possible.
*
* @return void
*/
public function createShorthands()
{
foreach ($this->getAllDeclarationBlocks() as $oDeclaration) {
$oDeclaration->createShorthands();
}
}
// Override render() to make format argument optional
public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat = null) {
if($oOutputFormat === null) {
$oOutputFormat = new \Sabberworm\CSS\OutputFormat();
}
return parent::render($oOutputFormat);
}
/**
* Overrides `render()` to make format argument optional.
*
* @param OutputFormat|null $oOutputFormat
*
* @return string
*/
public function render(OutputFormat $oOutputFormat = null)
{
if ($oOutputFormat === null) {
$oOutputFormat = new OutputFormat();
}
return parent::render($oOutputFormat);
}
public function isRootList() {
return true;
}
}
/**
* @return bool
*/
public function isRootList()
{
return true;
}
}

View File

@ -2,55 +2,103 @@
namespace Sabberworm\CSS\CSSList;
use Sabberworm\CSS\OutputFormat;
use Sabberworm\CSS\Property\AtRule;
class KeyFrame extends CSSList implements AtRule {
class KeyFrame extends CSSList implements AtRule
{
/**
* @var string|null
*/
private $vendorKeyFrame;
private $vendorKeyFrame;
private $animationName;
/**
* @var string|null
*/
private $animationName;
public function __construct($iLineNo = 0) {
parent::__construct($iLineNo);
$this->vendorKeyFrame = null;
$this->animationName = null;
}
/**
* @param int $iLineNo
*/
public function __construct($iLineNo = 0)
{
parent::__construct($iLineNo);
$this->vendorKeyFrame = null;
$this->animationName = null;
}
public function setVendorKeyFrame($vendorKeyFrame) {
$this->vendorKeyFrame = $vendorKeyFrame;
}
/**
* @param string $vendorKeyFrame
*/
public function setVendorKeyFrame($vendorKeyFrame)
{
$this->vendorKeyFrame = $vendorKeyFrame;
}
public function getVendorKeyFrame() {
return $this->vendorKeyFrame;
}
/**
* @return string|null
*/
public function getVendorKeyFrame()
{
return $this->vendorKeyFrame;
}
public function setAnimationName($animationName) {
$this->animationName = $animationName;
}
/**
* @param string $animationName
*/
public function setAnimationName($animationName)
{
$this->animationName = $animationName;
}
public function getAnimationName() {
return $this->animationName;
}
/**
* @return string|null
*/
public function getAnimationName()
{
return $this->animationName;
}
public function __toString() {
return $this->render(new \Sabberworm\CSS\OutputFormat());
}
/**
* @return string
*/
public function __toString()
{
return $this->render(new OutputFormat());
}
public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) {
$sResult = "@{$this->vendorKeyFrame} {$this->animationName}{$oOutputFormat->spaceBeforeOpeningBrace()}{";
$sResult .= parent::render($oOutputFormat);
$sResult .= '}';
return $sResult;
}
/**
* @return string
*/
public function render(OutputFormat $oOutputFormat)
{
$sResult = "@{$this->vendorKeyFrame} {$this->animationName}{$oOutputFormat->spaceBeforeOpeningBrace()}{";
$sResult .= parent::render($oOutputFormat);
$sResult .= '}';
return $sResult;
}
public function isRootList() {
return false;
}
/**
* @return bool
*/
public function isRootList()
{
return false;
}
public function atRuleName() {
return $this->vendorKeyFrame;
}
/**
* @return string|null
*/
public function atRuleName()
{
return $this->vendorKeyFrame;
}
public function atRuleArgs() {
return $this->animationName;
}
/**
* @return string|null
*/
public function atRuleArgs()
{
return $this->animationName;
}
}

View File

@ -2,50 +2,70 @@
namespace Sabberworm\CSS\Comment;
use Sabberworm\CSS\OutputFormat;
use Sabberworm\CSS\Renderable;
class Comment implements Renderable {
protected $iLineNo;
protected $sComment;
class Comment implements Renderable
{
/**
* @var int
*/
protected $iLineNo;
public function __construct($sComment = '', $iLineNo = 0) {
$this->sComment = $sComment;
$this->iLineNo = $iLineNo;
}
/**
* @var string
*/
protected $sComment;
/**
* @return string
*/
public function getComment() {
return $this->sComment;
}
/**
* @param string $sComment
* @param int $iLineNo
*/
public function __construct($sComment = '', $iLineNo = 0)
{
$this->sComment = $sComment;
$this->iLineNo = $iLineNo;
}
/**
* @return int
*/
public function getLineNo() {
return $this->iLineNo;
}
/**
* @return string
*/
public function getComment()
{
return $this->sComment;
}
/**
* @return string
*/
public function setComment($sComment) {
$this->sComment = $sComment;
}
/**
* @return int
*/
public function getLineNo()
{
return $this->iLineNo;
}
/**
* @return string
*/
public function __toString() {
return $this->render(new \Sabberworm\CSS\OutputFormat());
}
/**
* @param string $sComment
*
* @return void
*/
public function setComment($sComment)
{
$this->sComment = $sComment;
}
/**
* @return string
*/
public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) {
return '/*' . $this->sComment . '*/';
}
/**
* @return string
*/
public function __toString()
{
return $this->render(new OutputFormat());
}
/**
* @return string
*/
public function render(OutputFormat $oOutputFormat)
{
return '/*' . $this->sComment . '*/';
}
}

View File

@ -2,22 +2,24 @@
namespace Sabberworm\CSS\Comment;
interface Commentable {
/**
* @param array $aComments Array of comments.
*/
public function addComments(array $aComments);
/**
* @return array
*/
public function getComments();
/**
* @param array $aComments Array containing Comment objects.
*/
public function setComments(array $aComments);
interface Commentable
{
/**
* @param array<array-key, Comment> $aComments
*
* @return void
*/
public function addComments(array $aComments);
/**
* @return array<array-key, Comment>
*/
public function getComments();
/**
* @param array<array-key, Comment> $aComments
*
* @return void
*/
public function setComments(array $aComments);
}

View File

@ -2,321 +2,333 @@
namespace Sabberworm\CSS;
use Sabberworm\CSS\Parsing\OutputException;
/**
* Class OutputFormat
*
* @method OutputFormat setSemicolonAfterLastRule( bool $bSemicolonAfterLastRule ) Set whether semicolons are added after last rule.
* @method OutputFormat setSemicolonAfterLastRule(bool $bSemicolonAfterLastRule) Set whether semicolons are added after
* last rule.
*/
class OutputFormat {
/**
* Value format
*/
// " means double-quote, ' means single-quote
public $sStringQuotingType = '"';
// Output RGB colors in hash notation if possible
public $bRGBHashNotation = true;
/**
* Declaration format
*/
// Semicolon after the last rule of a declaration block can be omitted. To do that, set this false.
public $bSemicolonAfterLastRule = true;
/**
* Spacing
* Note that these strings are not sanity-checked: the value should only consist of whitespace
* Any newline character will be indented according to the current level.
* The triples (After, Before, Between) can be set using a wildcard (e.g. `$oFormat->set('Space*Rules', "\n");`)
*/
public $sSpaceAfterRuleName = ' ';
class OutputFormat
{
/**
* Value format: `"` means double-quote, `'` means single-quote
*
* @var string
*/
public $sStringQuotingType = '"';
public $sSpaceBeforeRules = '';
public $sSpaceAfterRules = '';
public $sSpaceBetweenRules = '';
/**
* Output RGB colors in hash notation if possible
*
* @var string
*/
public $bRGBHashNotation = true;
public $sSpaceBeforeBlocks = '';
public $sSpaceAfterBlocks = '';
public $sSpaceBetweenBlocks = "\n";
/**
* Declaration format
*
* Semicolon after the last rule of a declaration block can be omitted. To do that, set this false.
*
* @var bool
*/
public $bSemicolonAfterLastRule = true;
// Content injected in and around @-rule blocks.
public $sBeforeAtRuleBlock = '';
public $sAfterAtRuleBlock = '';
/**
* Spacing
* Note that these strings are not sanity-checked: the value should only consist of whitespace
* Any newline character will be indented according to the current level.
* The triples (After, Before, Between) can be set using a wildcard (e.g. `$oFormat->set('Space*Rules', "\n");`)
*/
public $sSpaceAfterRuleName = ' ';
// This is whats printed before and after the comma if a declaration block contains multiple selectors.
public $sSpaceBeforeSelectorSeparator = '';
public $sSpaceAfterSelectorSeparator = ' ';
// This is whats printed after the comma of value lists
public $sSpaceBeforeListArgumentSeparator = '';
public $sSpaceAfterListArgumentSeparator = '';
public $sSpaceBeforeOpeningBrace = ' ';
/**
* @var string
*/
public $sSpaceBeforeRules = '';
// Content injected in and around declaration blocks.
public $sBeforeDeclarationBlock = '';
public $sAfterDeclarationBlockSelectors = '';
public $sAfterDeclarationBlock = '';
/**
* @var string
*/
public $sSpaceAfterRules = '';
/**
* Indentation
*/
// Indentation character(s) per level. Only applicable if newlines are used in any of the spacing settings.
public $sIndentation = "\t";
/**
* Output exceptions.
*/
public $bIgnoreExceptions = false;
private $oFormatter = null;
private $oNextLevelFormat = null;
private $iIndentationLevel = 0;
public function __construct() {
}
public function get($sName) {
$aVarPrefixes = array('a', 's', 'm', 'b', 'f', 'o', 'c', 'i');
foreach($aVarPrefixes as $sPrefix) {
$sFieldName = $sPrefix.ucfirst($sName);
if(isset($this->$sFieldName)) {
return $this->$sFieldName;
}
}
return null;
}
public function set($aNames, $mValue) {
$aVarPrefixes = array('a', 's', 'm', 'b', 'f', 'o', 'c', 'i');
if(is_string($aNames) && strpos($aNames, '*') !== false) {
$aNames = array(str_replace('*', 'Before', $aNames), str_replace('*', 'Between', $aNames), str_replace('*', 'After', $aNames));
} else if(!is_array($aNames)) {
$aNames = array($aNames);
}
foreach($aVarPrefixes as $sPrefix) {
$bDidReplace = false;
foreach($aNames as $sName) {
$sFieldName = $sPrefix.ucfirst($sName);
if(isset($this->$sFieldName)) {
$this->$sFieldName = $mValue;
$bDidReplace = true;
}
}
if($bDidReplace) {
return $this;
}
}
// Break the chain so the user knows this option is invalid
return false;
}
public function __call($sMethodName, $aArguments) {
if(strpos($sMethodName, 'set') === 0) {
return $this->set(substr($sMethodName, 3), $aArguments[0]);
} else if(strpos($sMethodName, 'get') === 0) {
return $this->get(substr($sMethodName, 3));
} else if(method_exists('\\Sabberworm\\CSS\\OutputFormatter', $sMethodName)) {
return call_user_func_array(array($this->getFormatter(), $sMethodName), $aArguments);
} else {
throw new \Exception('Unknown OutputFormat method called: '.$sMethodName);
}
}
public function indentWithTabs($iNumber = 1) {
return $this->setIndentation(str_repeat("\t", $iNumber));
}
public function indentWithSpaces($iNumber = 2) {
return $this->setIndentation(str_repeat(" ", $iNumber));
}
public function nextLevel() {
if($this->oNextLevelFormat === null) {
$this->oNextLevelFormat = clone $this;
$this->oNextLevelFormat->iIndentationLevel++;
$this->oNextLevelFormat->oFormatter = null;
}
return $this->oNextLevelFormat;
}
public function beLenient() {
$this->bIgnoreExceptions = true;
}
public function getFormatter() {
if($this->oFormatter === null) {
$this->oFormatter = new OutputFormatter($this);
}
return $this->oFormatter;
}
public function level() {
return $this->iIndentationLevel;
}
/**
* @var string
*/
public $sSpaceBetweenRules = '';
/**
* Create format.
*
* @return OutputFormat Format.
*/
public static function create() {
return new OutputFormat();
}
/**
* @var string
*/
public $sSpaceBeforeBlocks = '';
/**
* Create compact format.
*
* @return OutputFormat Format.
*/
public static function createCompact() {
$format = self::create();
$format->set('Space*Rules', "")->set('Space*Blocks', "")->setSpaceAfterRuleName('')->setSpaceBeforeOpeningBrace('')->setSpaceAfterSelectorSeparator('');
return $format;
}
/**
* @var string
*/
public $sSpaceAfterBlocks = '';
/**
* Create pretty format.
*
* @return OutputFormat Format.
*/
public static function createPretty() {
$format = self::create();
$format->set('Space*Rules', "\n")->set('Space*Blocks', "\n")->setSpaceBetweenBlocks("\n\n")->set('SpaceAfterListArgumentSeparator', array('default' => '', ',' => ' '));
return $format;
}
}
class OutputFormatter {
private $oFormat;
public function __construct(OutputFormat $oFormat) {
$this->oFormat = $oFormat;
}
public function space($sName, $sType = null) {
$sSpaceString = $this->oFormat->get("Space$sName");
// If $sSpaceString is an array, we have multple values configured depending on the type of object the space applies to
if(is_array($sSpaceString)) {
if($sType !== null && isset($sSpaceString[$sType])) {
$sSpaceString = $sSpaceString[$sType];
} else {
$sSpaceString = reset($sSpaceString);
}
}
return $this->prepareSpace($sSpaceString);
}
public function spaceAfterRuleName() {
return $this->space('AfterRuleName');
}
public function spaceBeforeRules() {
return $this->space('BeforeRules');
}
public function spaceAfterRules() {
return $this->space('AfterRules');
}
public function spaceBetweenRules() {
return $this->space('BetweenRules');
}
public function spaceBeforeBlocks() {
return $this->space('BeforeBlocks');
}
public function spaceAfterBlocks() {
return $this->space('AfterBlocks');
}
public function spaceBetweenBlocks() {
return $this->space('BetweenBlocks');
}
public function spaceBeforeSelectorSeparator() {
return $this->space('BeforeSelectorSeparator');
}
public function spaceAfterSelectorSeparator() {
return $this->space('AfterSelectorSeparator');
}
public function spaceBeforeListArgumentSeparator($sSeparator) {
return $this->space('BeforeListArgumentSeparator', $sSeparator);
}
public function spaceAfterListArgumentSeparator($sSeparator) {
return $this->space('AfterListArgumentSeparator', $sSeparator);
}
public function spaceBeforeOpeningBrace() {
return $this->space('BeforeOpeningBrace');
}
/**
* Runs the given code, either swallowing or passing exceptions, depending on the bIgnoreExceptions setting.
*/
public function safely($cCode) {
if($this->oFormat->get('IgnoreExceptions')) {
// If output exceptions are ignored, run the code with exception guards
try {
return $cCode();
} catch (OutputException $e) {
return null;
} //Do nothing
} else {
// Run the code as-is
return $cCode();
}
}
/**
* Clone of the implode function but calls ->render with the current output format instead of __toString()
*/
public function implode($sSeparator, $aValues, $bIncreaseLevel = false) {
$sResult = '';
$oFormat = $this->oFormat;
if($bIncreaseLevel) {
$oFormat = $oFormat->nextLevel();
}
$bIsFirst = true;
foreach($aValues as $mValue) {
if($bIsFirst) {
$bIsFirst = false;
} else {
$sResult .= $sSeparator;
}
if($mValue instanceof \Sabberworm\CSS\Renderable) {
$sResult .= $mValue->render($oFormat);
} else {
$sResult .= $mValue;
}
}
return $sResult;
}
public function removeLastSemicolon($sString) {
if($this->oFormat->get('SemicolonAfterLastRule')) {
return $sString;
}
$sString = explode(';', $sString);
if(count($sString) < 2) {
return $sString[0];
}
$sLast = array_pop($sString);
$sNextToLast = array_pop($sString);
array_push($sString, $sNextToLast.$sLast);
return implode(';', $sString);
}
private function prepareSpace($sSpaceString) {
return str_replace("\n", "\n".$this->indent(), $sSpaceString);
}
private function indent() {
return str_repeat($this->oFormat->sIndentation, $this->oFormat->level());
}
/**
* @var string
*/
public $sSpaceBetweenBlocks = "\n";
/**
* Content injected in and around at-rule blocks.
*
* @var string
*/
public $sBeforeAtRuleBlock = '';
/**
* @var string
*/
public $sAfterAtRuleBlock = '';
/**
* This is whats printed before and after the comma if a declaration block contains multiple selectors.
*
* @var string
*/
public $sSpaceBeforeSelectorSeparator = '';
/**
* @var string
*/
public $sSpaceAfterSelectorSeparator = ' ';
/**
* This is whats printed after the comma of value lists
*
* @var string
*/
public $sSpaceBeforeListArgumentSeparator = '';
/**
* @var string
*/
public $sSpaceAfterListArgumentSeparator = '';
/**
* @var string
*/
public $sSpaceBeforeOpeningBrace = ' ';
/**
* Content injected in and around declaration blocks.
*
* @var string
*/
public $sBeforeDeclarationBlock = '';
/**
* @var string
*/
public $sAfterDeclarationBlockSelectors = '';
/**
* @var string
*/
public $sAfterDeclarationBlock = '';
/**
* Indentation character(s) per level. Only applicable if newlines are used in any of the spacing settings.
*
* @var string
*/
public $sIndentation = "\t";
/**
* Output exceptions.
*
* @var bool
*/
public $bIgnoreExceptions = false;
/**
* @var OutputFormatter|null
*/
private $oFormatter = null;
/**
* @var OutputFormat|null
*/
private $oNextLevelFormat = null;
/**
* @var int
*/
private $iIndentationLevel = 0;
public function __construct()
{
}
/**
* @param string $sName
*
* @return string|null
*/
public function get($sName)
{
$aVarPrefixes = ['a', 's', 'm', 'b', 'f', 'o', 'c', 'i'];
foreach ($aVarPrefixes as $sPrefix) {
$sFieldName = $sPrefix . ucfirst($sName);
if (isset($this->$sFieldName)) {
return $this->$sFieldName;
}
}
return null;
}
/**
* @param array<array-key, string>|string $aNames
* @param mixed $mValue
*
* @return self|false
*/
public function set($aNames, $mValue)
{
$aVarPrefixes = ['a', 's', 'm', 'b', 'f', 'o', 'c', 'i'];
if (is_string($aNames) && strpos($aNames, '*') !== false) {
$aNames =
[
str_replace('*', 'Before', $aNames),
str_replace('*', 'Between', $aNames),
str_replace('*', 'After', $aNames),
];
} elseif (!is_array($aNames)) {
$aNames = [$aNames];
}
foreach ($aVarPrefixes as $sPrefix) {
$bDidReplace = false;
foreach ($aNames as $sName) {
$sFieldName = $sPrefix . ucfirst($sName);
if (isset($this->$sFieldName)) {
$this->$sFieldName = $mValue;
$bDidReplace = true;
}
}
if ($bDidReplace) {
return $this;
}
}
// Break the chain so the user knows this option is invalid
return false;
}
/**
* @param string $sMethodName
* @param array<array-key, mixed> $aArguments
*
* @return mixed
*
* @throws \Exception
*/
public function __call($sMethodName, array $aArguments)
{
if (strpos($sMethodName, 'set') === 0) {
return $this->set(substr($sMethodName, 3), $aArguments[0]);
} elseif (strpos($sMethodName, 'get') === 0) {
return $this->get(substr($sMethodName, 3));
} elseif (method_exists(OutputFormatter::class, $sMethodName)) {
return call_user_func_array([$this->getFormatter(), $sMethodName], $aArguments);
} else {
throw new \Exception('Unknown OutputFormat method called: ' . $sMethodName);
}
}
/**
* @param int $iNumber
*
* @return self
*/
public function indentWithTabs($iNumber = 1)
{
return $this->setIndentation(str_repeat("\t", $iNumber));
}
/**
* @param int $iNumber
*
* @return self
*/
public function indentWithSpaces($iNumber = 2)
{
return $this->setIndentation(str_repeat(" ", $iNumber));
}
/**
* @return OutputFormat
*/
public function nextLevel()
{
if ($this->oNextLevelFormat === null) {
$this->oNextLevelFormat = clone $this;
$this->oNextLevelFormat->iIndentationLevel++;
$this->oNextLevelFormat->oFormatter = null;
}
return $this->oNextLevelFormat;
}
/**
* @return void
*/
public function beLenient()
{
$this->bIgnoreExceptions = true;
}
/**
* @return OutputFormatter
*/
public function getFormatter()
{
if ($this->oFormatter === null) {
$this->oFormatter = new OutputFormatter($this);
}
return $this->oFormatter;
}
/**
* @return int
*/
public function level()
{
return $this->iIndentationLevel;
}
/**
* Creates an instance of this class without any particular formatting settings.
*
* @return self
*/
public static function create()
{
return new OutputFormat();
}
/**
* Creates an instance of this class with a preset for compact formatting.
*
* @return self
*/
public static function createCompact()
{
$format = self::create();
$format->set('Space*Rules', "")->set('Space*Blocks', "")->setSpaceAfterRuleName('')
->setSpaceBeforeOpeningBrace('')->setSpaceAfterSelectorSeparator('');
return $format;
}
/**
* Creates an instance of this class with a preset for pretty formatting.
*
* @return self
*/
public static function createPretty()
{
$format = self::create();
$format->set('Space*Rules', "\n")->set('Space*Blocks', "\n")
->setSpaceBetweenBlocks("\n\n")->set('SpaceAfterListArgumentSeparator', ['default' => '', ',' => ' ']);
return $format;
}
}

View File

@ -0,0 +1,231 @@
<?php
namespace Sabberworm\CSS;
use Sabberworm\CSS\Parsing\OutputException;
class OutputFormatter
{
/**
* @var OutputFormat
*/
private $oFormat;
public function __construct(OutputFormat $oFormat)
{
$this->oFormat = $oFormat;
}
/**
* @param string $sName
* @param string|null $sType
*
* @return string
*/
public function space($sName, $sType = null)
{
$sSpaceString = $this->oFormat->get("Space$sName");
// If $sSpaceString is an array, we have multiple values configured
// depending on the type of object the space applies to
if (is_array($sSpaceString)) {
if ($sType !== null && isset($sSpaceString[$sType])) {
$sSpaceString = $sSpaceString[$sType];
} else {
$sSpaceString = reset($sSpaceString);
}
}
return $this->prepareSpace($sSpaceString);
}
/**
* @return string
*/
public function spaceAfterRuleName()
{
return $this->space('AfterRuleName');
}
/**
* @return string
*/
public function spaceBeforeRules()
{
return $this->space('BeforeRules');
}
/**
* @return string
*/
public function spaceAfterRules()
{
return $this->space('AfterRules');
}
/**
* @return string
*/
public function spaceBetweenRules()
{
return $this->space('BetweenRules');
}
/**
* @return string
*/
public function spaceBeforeBlocks()
{
return $this->space('BeforeBlocks');
}
/**
* @return string
*/
public function spaceAfterBlocks()
{
return $this->space('AfterBlocks');
}
/**
* @return string
*/
public function spaceBetweenBlocks()
{
return $this->space('BetweenBlocks');
}
/**
* @return string
*/
public function spaceBeforeSelectorSeparator()
{
return $this->space('BeforeSelectorSeparator');
}
/**
* @return string
*/
public function spaceAfterSelectorSeparator()
{
return $this->space('AfterSelectorSeparator');
}
/**
* @param string $sSeparator
*
* @return string
*/
public function spaceBeforeListArgumentSeparator($sSeparator)
{
return $this->space('BeforeListArgumentSeparator', $sSeparator);
}
/**
* @param string $sSeparator
*
* @return string
*/
public function spaceAfterListArgumentSeparator($sSeparator)
{
return $this->space('AfterListArgumentSeparator', $sSeparator);
}
/**
* @return string
*/
public function spaceBeforeOpeningBrace()
{
return $this->space('BeforeOpeningBrace');
}
/**
* Runs the given code, either swallowing or passing exceptions, depending on the `bIgnoreExceptions` setting.
*
* @param string $cCode the name of the function to call
*
* @return string|null
*/
public function safely($cCode)
{
if ($this->oFormat->get('IgnoreExceptions')) {
// If output exceptions are ignored, run the code with exception guards
try {
return $cCode();
} catch (OutputException $e) {
return null;
} // Do nothing
} else {
// Run the code as-is
return $cCode();
}
}
/**
* Clone of the `implode` function, but calls `render` with the current output format instead of `__toString()`.
*
* @param string $sSeparator
* @param array<array-key, Renderable|string> $aValues
* @param bool $bIncreaseLevel
*
* @return string
*/
public function implode($sSeparator, array $aValues, $bIncreaseLevel = false)
{
$sResult = '';
$oFormat = $this->oFormat;
if ($bIncreaseLevel) {
$oFormat = $oFormat->nextLevel();
}
$bIsFirst = true;
foreach ($aValues as $mValue) {
if ($bIsFirst) {
$bIsFirst = false;
} else {
$sResult .= $sSeparator;
}
if ($mValue instanceof Renderable) {
$sResult .= $mValue->render($oFormat);
} else {
$sResult .= $mValue;
}
}
return $sResult;
}
/**
* @param string $sString
*
* @return string
*/
public function removeLastSemicolon($sString)
{
if ($this->oFormat->get('SemicolonAfterLastRule')) {
return $sString;
}
$sString = explode(';', $sString);
if (count($sString) < 2) {
return $sString[0];
}
$sLast = array_pop($sString);
$sNextToLast = array_pop($sString);
array_push($sString, $sNextToLast . $sLast);
return implode(';', $sString);
}
/**
* @param string $sSpaceString
*
* @return string
*/
private function prepareSpace($sSpaceString)
{
return str_replace("\n", "\n" . $this->indent(), $sSpaceString);
}
/**
* @return string
*/
private function indent()
{
return str_repeat($this->oFormat->sIndentation, $this->oFormat->level());
}
}

View File

@ -4,38 +4,57 @@ namespace Sabberworm\CSS;
use Sabberworm\CSS\CSSList\Document;
use Sabberworm\CSS\Parsing\ParserState;
use Sabberworm\CSS\Parsing\SourceException;
/**
* Parser class parses CSS from text into a data structure.
* This class parses CSS from text into a data structure.
*/
class Parser {
private $oParserState;
class Parser
{
/**
* @var ParserState
*/
private $oParserState;
/**
* Parser constructor.
* Note that that iLineNo starts from 1 and not 0
*
* @param $sText
* @param Settings|null $oParserSettings
* @param int $iLineNo
*/
public function __construct($sText, Settings $oParserSettings = null, $iLineNo = 1) {
if ($oParserSettings === null) {
$oParserSettings = Settings::create();
}
$this->oParserState = new ParserState($sText, $oParserSettings, $iLineNo);
}
/**
* @param string $sText
* @param Settings|null $oParserSettings
* @param int $iLineNo the line number (starting from 1, not from 0)
*/
public function __construct($sText, Settings $oParserSettings = null, $iLineNo = 1)
{
if ($oParserSettings === null) {
$oParserSettings = Settings::create();
}
$this->oParserState = new ParserState($sText, $oParserSettings, $iLineNo);
}
public function setCharset($sCharset) {
$this->oParserState->setCharset($sCharset);
}
/**
* @param string $sCharset
*
* @return void
*/
public function setCharset($sCharset)
{
$this->oParserState->setCharset($sCharset);
}
public function getCharset() {
$this->oParserState->getCharset();
}
public function parse() {
return Document::parse($this->oParserState);
}
/**
* @return void
*/
public function getCharset()
{
// Note: The `return` statement is missing here. This is a bug that needs to be fixed.
$this->oParserState->getCharset();
}
/**
* @return Document
*
* @throws SourceException
*/
public function parse()
{
return Document::parse($this->oParserState);
}
}

View File

@ -3,10 +3,16 @@
namespace Sabberworm\CSS\Parsing;
/**
* Thrown if the CSS parsers attempts to print something invalid
*/
class OutputException extends SourceException {
public function __construct($sMessage, $iLineNo = 0) {
parent::__construct($sMessage, $iLineNo);
}
}
* Thrown if the CSS parser attempts to print something invalid.
*/
class OutputException extends SourceException
{
/**
* @param string $sMessage
* @param int $iLineNo
*/
public function __construct($sMessage, $iLineNo = 0)
{
parent::__construct($sMessage, $iLineNo);
}
}

View File

@ -1,310 +1,516 @@
<?php
namespace Sabberworm\CSS\Parsing;
use Sabberworm\CSS\Comment\Comment;
use Sabberworm\CSS\Parsing\UnexpectedTokenException;
use Sabberworm\CSS\Settings;
class ParserState {
private $oParserSettings;
class ParserState
{
/**
* @var null
*/
const EOF = null;
private $sText;
/**
* @var Settings
*/
private $oParserSettings;
private $aText;
private $iCurrentPosition;
private $sCharset;
private $iLength;
private $iLineNo;
/**
* @var string
*/
private $sText;
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);
}
/**
* @var array<int, string>
*/
private $aText;
public function setCharset($sCharset) {
$this->sCharset = $sCharset;
$this->aText = $this->strsplit($this->sText);
$this->iLength = count($this->aText);
}
/**
* @var int
*/
private $iCurrentPosition;
public function getCharset() {
$this->oParserHelper->getCharset();
return $this->sCharset;
}
/**
* @var string
*/
private $sCharset;
public function currentLine() {
return $this->iLineNo;
}
/**
* @var int
*/
private $iLength;
public function getSettings() {
return $this->oParserSettings;
}
/**
* @var int
*/
private $iLineNo;
public function parseIdentifier($bIgnoreCase = true) {
$sResult = $this->parseCharacter(true);
if ($sResult === null) {
throw new UnexpectedTokenException($sResult, $this->peek(5), 'identifier', $this->iLineNo);
}
$sCharacter = null;
while (($sCharacter = $this->parseCharacter(true)) !== null) {
$sResult .= $sCharacter;
}
if ($bIgnoreCase) {
$sResult = $this->strtolower($sResult);
}
return $sResult;
}
/**
* @param string $sText
* @param int $iLineNo
*/
public function __construct($sText, Settings $oParserSettings, $iLineNo = 1)
{
$this->oParserSettings = $oParserSettings;
$this->sText = $sText;
$this->iCurrentPosition = 0;
$this->iLineNo = $iLineNo;
$this->setCharset($this->oParserSettings->sDefaultCharset);
}
public function parseCharacter($bIsForIdentifier) {
if ($this->peek() === '\\') {
if ($bIsForIdentifier && $this->oParserSettings->bLenientParsing && ($this->comes('\0') || $this->comes('\9'))) {
// Non-strings can contain \0 or \9 which is an IE hack supported in lenient parsing.
return null;
}
$this->consume('\\');
if ($this->comes('\n') || $this->comes('\r')) {
return '';
}
if (preg_match('/[0-9a-fA-F]/Su', $this->peek()) === 0) {
return $this->consume(1);
}
$sUnicode = $this->consumeExpression('/^[0-9a-fA-F]{1,6}/u', 6);
if ($this->strlen($sUnicode) < 6) {
//Consume whitespace after incomplete unicode escape
if (preg_match('/\\s/isSu', $this->peek())) {
if ($this->comes('\r\n')) {
$this->consume(2);
} else {
$this->consume(1);
}
}
}
$iUnicode = intval($sUnicode, 16);
$sUtf32 = "";
for ($i = 0; $i < 4; ++$i) {
$sUtf32 .= chr($iUnicode & 0xff);
$iUnicode = $iUnicode >> 8;
}
return iconv('utf-32le', $this->sCharset, $sUtf32);
}
if ($bIsForIdentifier) {
$peek = ord($this->peek());
// Ranges: a-z A-Z 0-9 - _
if (($peek >= 97 && $peek <= 122) ||
($peek >= 65 && $peek <= 90) ||
($peek >= 48 && $peek <= 57) ||
($peek === 45) ||
($peek === 95) ||
($peek > 0xa1)) {
return $this->consume(1);
}
} else {
return $this->consume(1);
}
return null;
}
/**
* @param string $sCharset
*
* @return void
*/
public function setCharset($sCharset)
{
$this->sCharset = $sCharset;
$this->aText = $this->strsplit($this->sText);
if (is_array($this->aText)) {
$this->iLength = count($this->aText);
}
}
public function consumeWhiteSpace() {
$comments = array();
do {
while (preg_match('/\\s/isSu', $this->peek()) === 1) {
$this->consume(1);
}
if($this->oParserSettings->bLenientParsing) {
try {
$oComment = $this->consumeComment();
} catch(UnexpectedTokenException $e) {
// When we cant find the end of a comment, we assume the document is finished.
$this->iCurrentPosition = $this->iLength;
return;
}
} else {
$oComment = $this->consumeComment();
}
if ($oComment !== false) {
$comments[] = $oComment;
}
} while($oComment !== false);
return $comments;
}
/**
* @return string
*/
public function getCharset()
{
return $this->sCharset;
}
public function comes($sString, $bCaseInsensitive = false) {
$sPeek = $this->peek(strlen($sString));
return ($sPeek == '')
? false
: $this->streql($sPeek, $sString, $bCaseInsensitive);
}
/**
* @return int
*/
public function currentLine()
{
return $this->iLineNo;
}
public function peek($iLength = 1, $iOffset = 0) {
$iOffset += $this->iCurrentPosition;
if ($iOffset >= $this->iLength) {
return '';
}
return $this->substr($iOffset, $iLength);
}
/**
* @return int
*/
public function currentColumn()
{
return $this->iCurrentPosition;
}
public function consume($mValue = 1) {
if (is_string($mValue)) {
$iLineCount = substr_count($mValue, "\n");
$iLength = $this->strlen($mValue);
if (!$this->streql($this->substr($this->iCurrentPosition, $iLength), $mValue)) {
throw new UnexpectedTokenException($mValue, $this->peek(max($iLength, 5)), $this->iLineNo);
}
$this->iLineNo += $iLineCount;
$this->iCurrentPosition += $this->strlen($mValue);
return $mValue;
} else {
if ($this->iCurrentPosition + $mValue > $this->iLength) {
throw new UnexpectedTokenException($mValue, $this->peek(5), 'count', $this->iLineNo);
}
$sResult = $this->substr($this->iCurrentPosition, $mValue);
$iLineCount = substr_count($sResult, "\n");
$this->iLineNo += $iLineCount;
$this->iCurrentPosition += $mValue;
return $sResult;
}
}
/**
* @return Settings
*/
public function getSettings()
{
return $this->oParserSettings;
}
public function consumeExpression($mExpression, $iMaxLength = null) {
$aMatches = null;
$sInput = $iMaxLength !== null ? $this->peek($iMaxLength) : $this->inputLeft();
if (preg_match($mExpression, $sInput, $aMatches, PREG_OFFSET_CAPTURE) === 1) {
return $this->consume($aMatches[0][0]);
}
throw new UnexpectedTokenException($mExpression, $this->peek(5), 'expression', $this->iLineNo);
}
/**
* @param bool $bIgnoreCase
*
* @return string
*
* @throws UnexpectedTokenException
*/
public function parseIdentifier($bIgnoreCase = true)
{
$sResult = $this->parseCharacter(true);
if ($sResult === null) {
throw new UnexpectedTokenException($sResult, $this->peek(5), 'identifier', $this->iLineNo);
}
$sCharacter = null;
while (($sCharacter = $this->parseCharacter(true)) !== null) {
if (preg_match('/[a-zA-Z0-9\x{00A0}-\x{FFFF}_-]/Sux', $sCharacter)) {
$sResult .= $sCharacter;
} else {
$sResult .= '\\' . $sCharacter;
}
}
if ($bIgnoreCase) {
$sResult = $this->strtolower($sResult);
}
return $sResult;
}
/**
* @return false|Comment
*/
public function consumeComment() {
$mComment = false;
if ($this->comes('/*')) {
$iLineNo = $this->iLineNo;
$this->consume(1);
$mComment = '';
while (($char = $this->consume(1)) !== '') {
$mComment .= $char;
if ($this->comes('*/')) {
$this->consume(2);
break;
}
}
}
/**
* @param bool $bIsForIdentifier
*
* @return string|null
*
* @throws UnexpectedEOFException
* @throws UnexpectedTokenException
*/
public function parseCharacter($bIsForIdentifier)
{
if ($this->peek() === '\\') {
if (
$bIsForIdentifier && $this->oParserSettings->bLenientParsing
&& ($this->comes('\0') || $this->comes('\9'))
) {
// Non-strings can contain \0 or \9 which is an IE hack supported in lenient parsing.
return null;
}
$this->consume('\\');
if ($this->comes('\n') || $this->comes('\r')) {
return '';
}
if (preg_match('/[0-9a-fA-F]/Su', $this->peek()) === 0) {
return $this->consume(1);
}
$sUnicode = $this->consumeExpression('/^[0-9a-fA-F]{1,6}/u', 6);
if ($this->strlen($sUnicode) < 6) {
// Consume whitespace after incomplete unicode escape
if (preg_match('/\\s/isSu', $this->peek())) {
if ($this->comes('\r\n')) {
$this->consume(2);
} else {
$this->consume(1);
}
}
}
$iUnicode = intval($sUnicode, 16);
$sUtf32 = "";
for ($i = 0; $i < 4; ++$i) {
$sUtf32 .= chr($iUnicode & 0xff);
$iUnicode = $iUnicode >> 8;
}
return iconv('utf-32le', $this->sCharset, $sUtf32);
}
if ($bIsForIdentifier) {
$peek = ord($this->peek());
// Ranges: a-z A-Z 0-9 - _
if (
($peek >= 97 && $peek <= 122)
|| ($peek >= 65 && $peek <= 90)
|| ($peek >= 48 && $peek <= 57)
|| ($peek === 45)
|| ($peek === 95)
|| ($peek > 0xa1)
) {
return $this->consume(1);
}
} else {
return $this->consume(1);
}
return null;
}
if ($mComment !== false) {
// We skip the * which was included in the comment.
return new Comment(substr($mComment, 1), $iLineNo);
}
/**
* @return array<int, Comment>|void
*
* @throws UnexpectedEOFException
* @throws UnexpectedTokenException
*/
public function consumeWhiteSpace()
{
$comments = [];
do {
while (preg_match('/\\s/isSu', $this->peek()) === 1) {
$this->consume(1);
}
if ($this->oParserSettings->bLenientParsing) {
try {
$oComment = $this->consumeComment();
} catch (UnexpectedEOFException $e) {
$this->iCurrentPosition = $this->iLength;
return;
}
} else {
$oComment = $this->consumeComment();
}
if ($oComment !== false) {
$comments[] = $oComment;
}
} while ($oComment !== false);
return $comments;
}
return $mComment;
}
/**
* @param string $sString
* @param bool $bCaseInsensitive
*
* @return bool
*/
public function comes($sString, $bCaseInsensitive = false)
{
$sPeek = $this->peek(strlen($sString));
return ($sPeek == '')
? false
: $this->streql($sPeek, $sString, $bCaseInsensitive);
}
public function isEnd() {
return $this->iCurrentPosition >= $this->iLength;
}
/**
* @param int $iLength
* @param int $iOffset
*
* @return string
*/
public function peek($iLength = 1, $iOffset = 0)
{
$iOffset += $this->iCurrentPosition;
if ($iOffset >= $this->iLength) {
return '';
}
return $this->substr($iOffset, $iLength);
}
public function consumeUntil($aEnd, $bIncludeEnd = false, $consumeEnd = false, array &$comments = array()) {
$aEnd = is_array($aEnd) ? $aEnd : array($aEnd);
$out = '';
$start = $this->iCurrentPosition;
/**
* @param int $mValue
*
* @return string
*
* @throws UnexpectedEOFException
* @throws UnexpectedTokenException
*/
public function consume($mValue = 1)
{
if (is_string($mValue)) {
$iLineCount = substr_count($mValue, "\n");
$iLength = $this->strlen($mValue);
if (!$this->streql($this->substr($this->iCurrentPosition, $iLength), $mValue)) {
throw new UnexpectedTokenException($mValue, $this->peek(max($iLength, 5)), $this->iLineNo);
}
$this->iLineNo += $iLineCount;
$this->iCurrentPosition += $this->strlen($mValue);
return $mValue;
} else {
if ($this->iCurrentPosition + $mValue > $this->iLength) {
throw new UnexpectedEOFException($mValue, $this->peek(5), 'count', $this->iLineNo);
}
$sResult = $this->substr($this->iCurrentPosition, $mValue);
$iLineCount = substr_count($sResult, "\n");
$this->iLineNo += $iLineCount;
$this->iCurrentPosition += $mValue;
return $sResult;
}
}
while (($char = $this->consume(1)) !== '') {
if (in_array($char, $aEnd)) {
if ($bIncludeEnd) {
$out .= $char;
} elseif (!$consumeEnd) {
$this->iCurrentPosition -= $this->strlen($char);
}
return $out;
}
$out .= $char;
if ($comment = $this->consumeComment()) {
$comments[] = $comment;
}
}
/**
* @param string $mExpression
* @param int|null $iMaxLength
*
* @return string
*
* @throws UnexpectedEOFException
* @throws UnexpectedTokenException
*/
public function consumeExpression($mExpression, $iMaxLength = null)
{
$aMatches = null;
$sInput = $iMaxLength !== null ? $this->peek($iMaxLength) : $this->inputLeft();
if (preg_match($mExpression, $sInput, $aMatches, PREG_OFFSET_CAPTURE) === 1) {
return $this->consume($aMatches[0][0]);
}
throw new UnexpectedTokenException($mExpression, $this->peek(5), 'expression', $this->iLineNo);
}
$this->iCurrentPosition = $start;
throw new UnexpectedTokenException('One of ("'.implode('","', $aEnd).'")', $this->peek(5), 'search', $this->iLineNo);
}
/**
* @return Comment|false
*/
public function consumeComment()
{
$mComment = false;
if ($this->comes('/*')) {
$iLineNo = $this->iLineNo;
$this->consume(1);
$mComment = '';
while (($char = $this->consume(1)) !== '') {
$mComment .= $char;
if ($this->comes('*/')) {
$this->consume(2);
break;
}
}
}
private function inputLeft() {
return $this->substr($this->iCurrentPosition, -1);
}
if ($mComment !== false) {
// We skip the * which was included in the comment.
return new Comment(substr($mComment, 1), $iLineNo);
}
public function streql($sString1, $sString2, $bCaseInsensitive = true) {
if($bCaseInsensitive) {
return $this->strtolower($sString1) === $this->strtolower($sString2);
} else {
return $sString1 === $sString2;
}
}
return $mComment;
}
public function backtrack($iAmount) {
$this->iCurrentPosition -= $iAmount;
}
/**
* @return bool
*/
public function isEnd()
{
return $this->iCurrentPosition >= $this->iLength;
}
public function strlen($sString) {
if ($this->oParserSettings->bMultibyteSupport) {
return mb_strlen($sString, $this->sCharset);
} else {
return strlen($sString);
}
}
/**
* @param array<array-key, string>|string $aEnd
* @param string $bIncludeEnd
* @param string $consumeEnd
* @param array<int, Comment> $comments
*
* @return string
*
* @throws UnexpectedEOFException
* @throws UnexpectedTokenException
*/
public function consumeUntil($aEnd, $bIncludeEnd = false, $consumeEnd = false, array &$comments = [])
{
$aEnd = is_array($aEnd) ? $aEnd : [$aEnd];
$out = '';
$start = $this->iCurrentPosition;
private function substr($iStart, $iLength) {
if ($iLength < 0) {
$iLength = $this->iLength - $iStart + $iLength;
}
if ($iStart + $iLength > $this->iLength) {
$iLength = $this->iLength - $iStart;
}
$sResult = '';
while ($iLength > 0) {
$sResult .= $this->aText[$iStart];
$iStart++;
$iLength--;
}
return $sResult;
}
while (!$this->isEnd()) {
$char = $this->consume(1);
if (in_array($char, $aEnd)) {
if ($bIncludeEnd) {
$out .= $char;
} elseif (!$consumeEnd) {
$this->iCurrentPosition -= $this->strlen($char);
}
return $out;
}
$out .= $char;
if ($comment = $this->consumeComment()) {
$comments[] = $comment;
}
}
private function strtolower($sString) {
if ($this->oParserSettings->bMultibyteSupport) {
return mb_strtolower($sString, $this->sCharset);
} else {
return strtolower($sString);
}
}
if (in_array(self::EOF, $aEnd)) {
return $out;
}
private function strsplit($sString) {
if ($this->oParserSettings->bMultibyteSupport) {
if ($this->streql($this->sCharset, 'utf-8')) {
return preg_split('//u', $sString, null, PREG_SPLIT_NO_EMPTY);
} else {
$iLength = mb_strlen($sString, $this->sCharset);
$aResult = array();
for ($i = 0; $i < $iLength; ++$i) {
$aResult[] = mb_substr($sString, $i, 1, $this->sCharset);
}
return $aResult;
}
} else {
if($sString === '') {
return array();
} else {
return str_split($sString);
}
}
}
$this->iCurrentPosition = $start;
throw new UnexpectedEOFException(
'One of ("' . implode('","', $aEnd) . '")',
$this->peek(5),
'search',
$this->iLineNo
);
}
private function strpos($sString, $sNeedle, $iOffset) {
if ($this->oParserSettings->bMultibyteSupport) {
return mb_strpos($sString, $sNeedle, $iOffset, $this->sCharset);
} else {
return strpos($sString, $sNeedle, $iOffset);
}
}
}
/**
* @return string
*/
private function inputLeft()
{
return $this->substr($this->iCurrentPosition, -1);
}
/**
* @param string $sString1
* @param string $sString2
* @param bool $bCaseInsensitive
*
* @return bool
*/
public function streql($sString1, $sString2, $bCaseInsensitive = true)
{
if ($bCaseInsensitive) {
return $this->strtolower($sString1) === $this->strtolower($sString2);
} else {
return $sString1 === $sString2;
}
}
/**
* @param int $iAmount
*
* @return void
*/
public function backtrack($iAmount)
{
$this->iCurrentPosition -= $iAmount;
}
/**
* @param string $sString
*
* @return int
*/
public function strlen($sString)
{
if ($this->oParserSettings->bMultibyteSupport) {
return mb_strlen($sString, $this->sCharset);
} else {
return strlen($sString);
}
}
/**
* @param int $iStart
* @param int $iLength
*
* @return string
*/
private function substr($iStart, $iLength)
{
if ($iLength < 0) {
$iLength = $this->iLength - $iStart + $iLength;
}
if ($iStart + $iLength > $this->iLength) {
$iLength = $this->iLength - $iStart;
}
$sResult = '';
while ($iLength > 0) {
$sResult .= $this->aText[$iStart];
$iStart++;
$iLength--;
}
return $sResult;
}
/**
* @param string $sString
*
* @return string
*/
private function strtolower($sString)
{
if ($this->oParserSettings->bMultibyteSupport) {
return mb_strtolower($sString, $this->sCharset);
} else {
return strtolower($sString);
}
}
/**
* @param string $sString
*
* @return array<int, string>
*/
private function strsplit($sString)
{
if ($this->oParserSettings->bMultibyteSupport) {
if ($this->streql($this->sCharset, 'utf-8')) {
return preg_split('//u', $sString, -1, PREG_SPLIT_NO_EMPTY);
} else {
$iLength = mb_strlen($sString, $this->sCharset);
$aResult = [];
for ($i = 0; $i < $iLength; ++$i) {
$aResult[] = mb_substr($sString, $i, 1, $this->sCharset);
}
return $aResult;
}
} else {
if ($sString === '') {
return [];
} else {
return str_split($sString);
}
}
}
/**
* @param string $sString
* @param string $sNeedle
* @param int $iOffset
*
* @return int|false
*/
private function strpos($sString, $sNeedle, $iOffset)
{
if ($this->oParserSettings->bMultibyteSupport) {
return mb_strpos($sString, $sNeedle, $iOffset, $this->sCharset);
} else {
return strpos($sString, $sNeedle, $iOffset);
}
}
}

View File

@ -2,17 +2,31 @@
namespace Sabberworm\CSS\Parsing;
class SourceException extends \Exception {
private $iLineNo;
public function __construct($sMessage, $iLineNo = 0) {
$this->iLineNo = $iLineNo;
if (!empty($iLineNo)) {
$sMessage .= " [line no: $iLineNo]";
}
parent::__construct($sMessage);
}
class SourceException extends \Exception
{
/**
* @var int
*/
private $iLineNo;
public function getLineNo() {
return $this->iLineNo;
}
}
/**
* @param string $sMessage
* @param int $iLineNo
*/
public function __construct($sMessage, $iLineNo = 0)
{
$this->iLineNo = $iLineNo;
if (!empty($iLineNo)) {
$sMessage .= " [line no: $iLineNo]";
}
parent::__construct($sMessage);
}
/**
* @return int
*/
public function getLineNo()
{
return $this->iLineNo;
}
}

View File

@ -0,0 +1,12 @@
<?php
namespace Sabberworm\CSS\Parsing;
/**
* Thrown if the CSS parser encounters end of file it did not expect.
*
* Extends `UnexpectedTokenException` in order to preserve backwards compatibility.
*/
class UnexpectedEOFException extends UnexpectedTokenException
{
}

View File

@ -3,29 +3,49 @@
namespace Sabberworm\CSS\Parsing;
/**
* Thrown if the CSS parsers encounters a token it did not expect
*/
class UnexpectedTokenException extends SourceException {
private $sExpected;
private $sFound;
// Possible values: literal, identifier, count, expression, search
private $sMatchType;
* Thrown if the CSS parser encounters a token it did not expect.
*/
class UnexpectedTokenException extends SourceException
{
/**
* @var string
*/
private $sExpected;
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}”.";
} else if($this->sMatchType === 'count') {
$sMessage = "Next token was expected to have {$sExpected} chars. Context: “{$sFound}”.";
} else if($this->sMatchType === 'identifier') {
$sMessage = "Identifier expected. Got “{$sFound}";
} else if($this->sMatchType === 'custom') {
$sMessage = trim("$sExpected $sFound");
}
/**
* @var string
*/
private $sFound;
parent::__construct($sMessage, $iLineNo);
}
}
/**
* Possible values: literal, identifier, count, expression, search
*
* @var string
*/
private $sMatchType;
/**
* @param string $sExpected
* @param string $sFound
* @param string $sMatchType
* @param int $iLineNo
*/
public function __construct($sExpected, $sFound, $sMatchType = 'literal', $iLineNo = 0)
{
$this->sExpected = $sExpected;
$this->sFound = $sFound;
$this->sMatchType = $sMatchType;
$sMessage = "Token “{$sExpected}” ({$sMatchType}) not found. Got “{$sFound}”.";
if ($this->sMatchType === 'search') {
$sMessage = "Search for “{$sExpected}” returned no results. Context: “{$sFound}”.";
} elseif ($this->sMatchType === 'count') {
$sMessage = "Next token was expected to have {$sExpected} chars. Context: “{$sFound}”.";
} elseif ($this->sMatchType === 'identifier') {
$sMessage = "Identifier expected. Got “{$sFound}";
} elseif ($this->sMatchType === 'custom') {
$sMessage = trim("$sExpected $sFound");
}
parent::__construct($sMessage, $iLineNo);
}
}

View File

@ -2,15 +2,33 @@
namespace Sabberworm\CSS\Property;
use Sabberworm\CSS\Renderable;
use Sabberworm\CSS\Comment\Commentable;
use Sabberworm\CSS\Renderable;
interface AtRule extends Renderable, Commentable {
// Since there are more set rules than block rules, were whitelisting the block rules and have anything else be treated as a set rule.
const BLOCK_RULES = 'media/document/supports/region-style/font-feature-values';
// …and more font-specific ones (to be used inside font-feature-values)
const SET_RULES = 'font-face/counter-style/page/swash/styleset/annotation';
public function atRuleName();
public function atRuleArgs();
}
interface AtRule extends Renderable, Commentable
{
/**
* Since there are more set rules than block rules,
* were whitelisting the block rules and have anything else be treated as a set rule.
*
* @var string
*/
const BLOCK_RULES = 'media/document/supports/region-style/font-feature-values';
/**
* and more font-specific ones (to be used inside font-feature-values)
*
* @var string
*/
const SET_RULES = 'font-face/counter-style/page/swash/styleset/annotation';
/**
* @return string|null
*/
public function atRuleName();
/**
* @return string|null
*/
public function atRuleArgs();
}

View File

@ -2,74 +2,153 @@
namespace Sabberworm\CSS\Property;
use Sabberworm\CSS\Comment\Comment;
use Sabberworm\CSS\OutputFormat;
/**
* CSSNamespace represents an @namespace rule.
*/
class CSSNamespace implements AtRule {
private $mUrl;
private $sPrefix;
private $iLineNo;
protected $aComments;
public function __construct($mUrl, $sPrefix = null, $iLineNo = 0) {
$this->mUrl = $mUrl;
$this->sPrefix = $sPrefix;
$this->iLineNo = $iLineNo;
$this->aComments = array();
}
* `CSSNamespace` represents an `@namespace` rule.
*/
class CSSNamespace implements AtRule
{
/**
* @var string
*/
private $mUrl;
/**
* @return int
*/
public function getLineNo() {
return $this->iLineNo;
}
/**
* @var string
*/
private $sPrefix;
public function __toString() {
return $this->render(new \Sabberworm\CSS\OutputFormat());
}
/**
* @var int
*/
private $iLineNo;
public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) {
return '@namespace '.($this->sPrefix === null ? '' : $this->sPrefix.' ').$this->mUrl->render($oOutputFormat).';';
}
public function getUrl() {
return $this->mUrl;
}
/**
* @var array<array-key, Comment>
*/
protected $aComments;
public function getPrefix() {
return $this->sPrefix;
}
/**
* @param string $mUrl
* @param string|null $sPrefix
* @param int $iLineNo
*/
public function __construct($mUrl, $sPrefix = null, $iLineNo = 0)
{
$this->mUrl = $mUrl;
$this->sPrefix = $sPrefix;
$this->iLineNo = $iLineNo;
$this->aComments = [];
}
public function setUrl($mUrl) {
$this->mUrl = $mUrl;
}
/**
* @return int
*/
public function getLineNo()
{
return $this->iLineNo;
}
public function setPrefix($sPrefix) {
$this->sPrefix = $sPrefix;
}
/**
* @return string
*/
public function __toString()
{
return $this->render(new OutputFormat());
}
public function atRuleName() {
return 'namespace';
}
/**
* @return string
*/
public function render(OutputFormat $oOutputFormat)
{
return '@namespace ' . ($this->sPrefix === null ? '' : $this->sPrefix . ' ')
. $this->mUrl->render($oOutputFormat) . ';';
}
public function atRuleArgs() {
$aResult = array($this->mUrl);
if($this->sPrefix) {
array_unshift($aResult, $this->sPrefix);
}
return $aResult;
}
/**
* @return string
*/
public function getUrl()
{
return $this->mUrl;
}
public function addComments(array $aComments) {
$this->aComments = array_merge($this->aComments, $aComments);
}
/**
* @return string|null
*/
public function getPrefix()
{
return $this->sPrefix;
}
public function getComments() {
return $this->aComments;
}
/**
* @param string $mUrl
*
* @return void
*/
public function setUrl($mUrl)
{
$this->mUrl = $mUrl;
}
public function setComments(array $aComments) {
$this->aComments = $aComments;
}
}
/**
* @param string $sPrefix
*
* @return void
*/
public function setPrefix($sPrefix)
{
$this->sPrefix = $sPrefix;
}
/**
* @return string
*/
public function atRuleName()
{
return 'namespace';
}
/**
* @return array<int, string>
*/
public function atRuleArgs()
{
$aResult = [$this->mUrl];
if ($this->sPrefix) {
array_unshift($aResult, $this->sPrefix);
}
return $aResult;
}
/**
* @param array<array-key, Comment> $aComments
*
* @return void
*/
public function addComments(array $aComments)
{
$this->aComments = array_merge($this->aComments, $aComments);
}
/**
* @return array<array-key, Comment>
*/
public function getComments()
{
return $this->aComments;
}
/**
* @param array<array-key, Comment> $aComments
*
* @return void
*/
public function setComments(array $aComments)
{
$this->aComments = $aComments;
}
}

View File

@ -2,65 +2,128 @@
namespace Sabberworm\CSS\Property;
use Sabberworm\CSS\Comment\Comment;
use Sabberworm\CSS\OutputFormat;
/**
* Class representing an @charset rule.
* Class representing an `@charset` rule.
*
* The following restrictions apply:
* May not be found in any CSSList other than the Document.
* May only appear at the very top of a Documents contents.
* Must not appear more than once.
* - May not be found in any CSSList other than the Document.
* - May only appear at the very top of a Documents contents.
* - Must not appear more than once.
*/
class Charset implements AtRule {
class Charset implements AtRule
{
/**
* @var string
*/
private $sCharset;
private $sCharset;
protected $iLineNo;
protected $aComment;
/**
* @var int
*/
protected $iLineNo;
public function __construct($sCharset, $iLineNo = 0) {
$this->sCharset = $sCharset;
$this->iLineNo = $iLineNo;
$this->aComments = array();
}
/**
* @var array<array-key, Comment>
*/
protected $aComments;
/**
* @return int
*/
public function getLineNo() {
return $this->iLineNo;
}
/**
* @param string $sCharset
* @param int $iLineNo
*/
public function __construct($sCharset, $iLineNo = 0)
{
$this->sCharset = $sCharset;
$this->iLineNo = $iLineNo;
$this->aComments = [];
}
public function setCharset($sCharset) {
$this->sCharset = $sCharset;
}
/**
* @return int
*/
public function getLineNo()
{
return $this->iLineNo;
}
public function getCharset() {
return $this->sCharset;
}
/**
* @param string $sCharset
*
* @return void
*/
public function setCharset($sCharset)
{
$this->sCharset = $sCharset;
}
public function __toString() {
return $this->render(new \Sabberworm\CSS\OutputFormat());
}
/**
* @return string
*/
public function getCharset()
{
return $this->sCharset;
}
public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) {
return "@charset {$this->sCharset->render($oOutputFormat)};";
}
/**
* @return string
*/
public function __toString()
{
return $this->render(new OutputFormat());
}
public function atRuleName() {
return 'charset';
}
/**
* @return string
*/
public function render(OutputFormat $oOutputFormat)
{
return "@charset {$this->sCharset->render($oOutputFormat)};";
}
public function atRuleArgs() {
return $this->sCharset;
}
/**
* @return string
*/
public function atRuleName()
{
return 'charset';
}
public function addComments(array $aComments) {
$this->aComments = array_merge($this->aComments, $aComments);
}
/**
* @return string
*/
public function atRuleArgs()
{
return $this->sCharset;
}
public function getComments() {
return $this->aComments;
}
/**
* @param array<array-key, Comment> $aComments
*
* @return void
*/
public function addComments(array $aComments)
{
$this->aComments = array_merge($this->aComments, $aComments);
}
public function setComments(array $aComments) {
$this->aComments = $aComments;
}
}
/**
* @return array<array-key, Comment>
*/
public function getComments()
{
return $this->aComments;
}
/**
* @param array<array-key, Comment> $aComments
*
* @return void
*/
public function setComments(array $aComments)
{
$this->aComments = $aComments;
}
}

View File

@ -2,68 +2,136 @@
namespace Sabberworm\CSS\Property;
use Sabberworm\CSS\Comment\Comment;
use Sabberworm\CSS\OutputFormat;
use Sabberworm\CSS\Value\URL;
/**
* Class representing an @import rule.
*/
class Import implements AtRule {
private $oLocation;
private $sMediaQuery;
protected $iLineNo;
protected $aComments;
public function __construct(URL $oLocation, $sMediaQuery, $iLineNo = 0) {
$this->oLocation = $oLocation;
$this->sMediaQuery = $sMediaQuery;
$this->iLineNo = $iLineNo;
$this->aComments = array();
}
* Class representing an `@import` rule.
*/
class Import implements AtRule
{
/**
* @var URL
*/
private $oLocation;
/**
* @return int
*/
public function getLineNo() {
return $this->iLineNo;
}
/**
* @var string
*/
private $sMediaQuery;
public function setLocation($oLocation) {
$this->oLocation = $oLocation;
}
/**
* @var int
*/
protected $iLineNo;
public function getLocation() {
return $this->oLocation;
}
public function __toString() {
return $this->render(new \Sabberworm\CSS\OutputFormat());
}
/**
* @var array<array-key, Comment>
*/
protected $aComments;
public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) {
return "@import ".$this->oLocation->render($oOutputFormat).($this->sMediaQuery === null ? '' : ' '.$this->sMediaQuery).';';
}
/**
* @param URL $oLocation
* @param string $sMediaQuery
* @param int $iLineNo
*/
public function __construct(URL $oLocation, $sMediaQuery, $iLineNo = 0)
{
$this->oLocation = $oLocation;
$this->sMediaQuery = $sMediaQuery;
$this->iLineNo = $iLineNo;
$this->aComments = [];
}
public function atRuleName() {
return 'import';
}
/**
* @return int
*/
public function getLineNo()
{
return $this->iLineNo;
}
public function atRuleArgs() {
$aResult = array($this->oLocation);
if($this->sMediaQuery) {
array_push($aResult, $this->sMediaQuery);
}
return $aResult;
}
/**
* @param URL $oLocation
*
* @return void
*/
public function setLocation($oLocation)
{
$this->oLocation = $oLocation;
}
public function addComments(array $aComments) {
$this->aComments = array_merge($this->aComments, $aComments);
}
/**
* @return URL
*/
public function getLocation()
{
return $this->oLocation;
}
public function getComments() {
return $this->aComments;
}
/**
* @return string
*/
public function __toString()
{
return $this->render(new OutputFormat());
}
public function setComments(array $aComments) {
$this->aComments = $aComments;
}
}
/**
* @return string
*/
public function render(OutputFormat $oOutputFormat)
{
return "@import " . $this->oLocation->render($oOutputFormat)
. ($this->sMediaQuery === null ? '' : ' ' . $this->sMediaQuery) . ';';
}
/**
* @return string
*/
public function atRuleName()
{
return 'import';
}
/**
* @return array<int, URL|string>
*/
public function atRuleArgs()
{
$aResult = [$this->oLocation];
if ($this->sMediaQuery) {
array_push($aResult, $this->sMediaQuery);
}
return $aResult;
}
/**
* @param array<array-key, Comment> $aComments
*
* @return void
*/
public function addComments(array $aComments)
{
$this->aComments = array_merge($this->aComments, $aComments);
}
/**
* @return array<array-key, Comment>
*/
public function getComments()
{
return $this->aComments;
}
/**
* @param array<array-key, Comment> $aComments
*
* @return void
*/
public function setComments(array $aComments)
{
$this->aComments = $aComments;
}
}

View File

@ -0,0 +1,23 @@
<?php
namespace Sabberworm\CSS\Property;
class KeyframeSelector extends Selector
{
/**
* regexp for specificity calculations
*
* @var string
*/
const SELECTOR_VALIDATION_RX = '/
^(
(?:
[a-zA-Z0-9\x{00A0}-\x{FFFF}_^$|*="\'~\[\]()\-\s\.:#+>]* # any sequence of valid unescaped characters
(?:\\\\.)? # a single escaped character
(?:([\'"]).*?(?<!\\\\)\2)? # a quoted text like [id="example"]
)*
)|
(\d+%) # keyframe animation progress percentage (e.g. 50%)
$
/ux';
}

View File

@ -3,72 +3,136 @@
namespace Sabberworm\CSS\Property;
/**
* Class representing a single CSS selector. Selectors have to be split by the comma prior to being passed into this class.
* Class representing a single CSS selector. Selectors have to be split by the comma prior to being passed into this
* class.
*/
class Selector {
class Selector
{
/**
* regexp for specificity calculations
*
* @var string
*/
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';
//Regexes for specificity calculations
const NON_ID_ATTRIBUTES_AND_PSEUDO_CLASSES_RX = '/
(\.[\w]+) # classes
|
\[(\w+) # attributes
|
(\:( # pseudo classes
link|visited|active
|hover|focus
|lang
|target
|enabled|disabled|checked|indeterminate
|root
|nth-child|nth-last-child|nth-of-type|nth-last-of-type
|first-child|last-child|first-of-type|last-of-type
|only-child|only-of-type
|empty|contains
))
/ix';
/**
* regexp for specificity calculations
*
* @var string
*/
const ELEMENTS_AND_PSEUDO_ELEMENTS_RX = '/
((^|[\s\+\>\~]+)[\w]+ # elements
|
\:{1,2}( # pseudo-elements
after|before|first-letter|first-line|selection
))
/ix';
const ELEMENTS_AND_PSEUDO_ELEMENTS_RX = '/
((^|[\s\+\>\~]+)[\w]+ # elements
|
\:{1,2}( # pseudo-elements
after|before|first-letter|first-line|selection
))
/ix';
/**
* regexp for specificity calculations
*
* @var string
*/
const SELECTOR_VALIDATION_RX = '/
^(
(?:
[a-zA-Z0-9\x{00A0}-\x{FFFF}_^$|*="\'~\[\]()\-\s\.:#+>]* # any sequence of valid unescaped characters
(?:\\\\.)? # a single escaped character
(?:([\'"]).*?(?<!\\\\)\2)? # a quoted text like [id="example"]
)*
)$
/ux';
private $sSelector;
private $iSpecificity;
/**
* @var string
*/
private $sSelector;
public function __construct($sSelector, $bCalculateSpecificity = false) {
$this->setSelector($sSelector);
if ($bCalculateSpecificity) {
$this->getSpecificity();
}
}
/**
* @var int|null
*/
private $iSpecificity;
public function getSelector() {
return $this->sSelector;
}
/**
* @param string $sSelector
*
* @return bool
*/
public static function isValid($sSelector)
{
return preg_match(static::SELECTOR_VALIDATION_RX, $sSelector);
}
public function setSelector($sSelector) {
$this->sSelector = trim($sSelector);
$this->iSpecificity = null;
}
/**
* @param string $sSelector
* @param bool $bCalculateSpecificity
*/
public function __construct($sSelector, $bCalculateSpecificity = false)
{
$this->setSelector($sSelector);
if ($bCalculateSpecificity) {
$this->getSpecificity();
}
}
public function __toString() {
return $this->getSelector();
}
/**
* @return string
*/
public function getSelector()
{
return $this->sSelector;
}
public function getSpecificity() {
if ($this->iSpecificity === null) {
$a = 0;
/// @todo should exclude \# as well as "#"
$aMatches = null;
$b = substr_count($this->sSelector, '#');
$c = preg_match_all(self::NON_ID_ATTRIBUTES_AND_PSEUDO_CLASSES_RX, $this->sSelector, $aMatches);
$d = preg_match_all(self::ELEMENTS_AND_PSEUDO_ELEMENTS_RX, $this->sSelector, $aMatches);
$this->iSpecificity = ($a * 1000) + ($b * 100) + ($c * 10) + $d;
}
return $this->iSpecificity;
}
/**
* @param string $sSelector
*
* @return void
*/
public function setSelector($sSelector)
{
$this->sSelector = trim($sSelector);
$this->iSpecificity = null;
}
/**
* @return string
*/
public function __toString()
{
return $this->getSelector();
}
/**
* @return int
*/
public function getSpecificity()
{
if ($this->iSpecificity === null) {
$a = 0;
/// @todo should exclude \# as well as "#"
$aMatches = null;
$b = substr_count($this->sSelector, '#');
$c = preg_match_all(self::NON_ID_ATTRIBUTES_AND_PSEUDO_CLASSES_RX, $this->sSelector, $aMatches);
$d = preg_match_all(self::ELEMENTS_AND_PSEUDO_ELEMENTS_RX, $this->sSelector, $aMatches);
$this->iSpecificity = ($a * 1000) + ($b * 100) + ($c * 10) + $d;
}
return $this->iSpecificity;
}
}

View File

@ -2,8 +2,20 @@
namespace Sabberworm\CSS;
interface Renderable {
public function __toString();
public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat);
public function getLineNo();
}
interface Renderable
{
/**
* @return string
*/
public function __toString();
/**
* @return string
*/
public function render(OutputFormat $oOutputFormat);
/**
* @return int
*/
public function getLineNo();
}

View File

@ -2,8 +2,12 @@
namespace Sabberworm\CSS\Rule;
use Sabberworm\CSS\Comment\Comment;
use Sabberworm\CSS\Comment\Commentable;
use Sabberworm\CSS\OutputFormat;
use Sabberworm\CSS\Parsing\ParserState;
use Sabberworm\CSS\Parsing\UnexpectedEOFException;
use Sabberworm\CSS\Parsing\UnexpectedTokenException;
use Sabberworm\CSS\Renderable;
use Sabberworm\CSS\Value\RuleValueList;
use Sabberworm\CSS\Value\Value;
@ -12,224 +16,376 @@ use Sabberworm\CSS\Value\Value;
* RuleSets contains Rule objects which always have a key and a value.
* In CSS, Rules are expressed as follows: “key: value[0][0] value[0][1], value[1][0] value[1][1];
*/
class Rule implements Renderable, Commentable {
class Rule implements Renderable, Commentable
{
/**
* @var string
*/
private $sRule;
private $sRule;
private $mValue;
private $bIsImportant;
private $aIeHack;
protected $iLineNo;
protected $aComments;
/**
* @var RuleValueList|null
*/
private $mValue;
public function __construct($sRule, $iLineNo = 0) {
$this->sRule = $sRule;
$this->mValue = null;
$this->bIsImportant = false;
$this->aIeHack = array();
$this->iLineNo = $iLineNo;
$this->aComments = array();
}
/**
* @var bool
*/
private $bIsImportant;
public static function parse(ParserState $oParserState) {
$aComments = $oParserState->consumeWhiteSpace();
$oRule = new Rule($oParserState->parseIdentifier(), $oParserState->currentLine());
$oRule->setComments($aComments);
$oRule->addComments($oParserState->consumeWhiteSpace());
$oParserState->consume(':');
$oValue = Value::parseValue($oParserState, self::listDelimiterForRule($oRule->getRule()));
$oRule->setValue($oValue);
if ($oParserState->getSettings()->bLenientParsing) {
while ($oParserState->comes('\\')) {
$oParserState->consume('\\');
$oRule->addIeHack($oParserState->consume());
$oParserState->consumeWhiteSpace();
}
}
$oParserState->consumeWhiteSpace();
if ($oParserState->comes('!')) {
$oParserState->consume('!');
$oParserState->consumeWhiteSpace();
$oParserState->consume('important');
$oRule->setIsImportant(true);
}
$oParserState->consumeWhiteSpace();
while ($oParserState->comes(';')) {
$oParserState->consume(';');
}
/**
* @var array<int, int>
*/
private $aIeHack;
return $oRule;
}
/**
* @var int
*/
protected $iLineNo;
private static function listDelimiterForRule($sRule) {
if (preg_match('/^font($|-)/', $sRule)) {
return array(',', '/', ' ');
}
return array(',', ' ', '/');
}
/**
* @var int
*/
protected $iColNo;
/**
* @return int
*/
public function getLineNo() {
return $this->iLineNo;
}
/**
* @var array<array-key, Comment>
*/
protected $aComments;
public function setRule($sRule) {
$this->sRule = $sRule;
}
/**
* @param string $sRule
* @param int $iLineNo
* @param int $iColNo
*/
public function __construct($sRule, $iLineNo = 0, $iColNo = 0)
{
$this->sRule = $sRule;
$this->mValue = null;
$this->bIsImportant = false;
$this->aIeHack = [];
$this->iLineNo = $iLineNo;
$this->iColNo = $iColNo;
$this->aComments = [];
}
public function getRule() {
return $this->sRule;
}
/**
* @return Rule
*
* @throws UnexpectedEOFException
* @throws UnexpectedTokenException
*/
public static function parse(ParserState $oParserState)
{
$aComments = $oParserState->consumeWhiteSpace();
$oRule = new Rule(
$oParserState->parseIdentifier(!$oParserState->comes("--")),
$oParserState->currentLine(),
$oParserState->currentColumn()
);
$oRule->setComments($aComments);
$oRule->addComments($oParserState->consumeWhiteSpace());
$oParserState->consume(':');
$oValue = Value::parseValue($oParserState, self::listDelimiterForRule($oRule->getRule()));
$oRule->setValue($oValue);
if ($oParserState->getSettings()->bLenientParsing) {
while ($oParserState->comes('\\')) {
$oParserState->consume('\\');
$oRule->addIeHack($oParserState->consume());
$oParserState->consumeWhiteSpace();
}
}
$oParserState->consumeWhiteSpace();
if ($oParserState->comes('!')) {
$oParserState->consume('!');
$oParserState->consumeWhiteSpace();
$oParserState->consume('important');
$oRule->setIsImportant(true);
}
$oParserState->consumeWhiteSpace();
while ($oParserState->comes(';')) {
$oParserState->consume(';');
}
public function getValue() {
return $this->mValue;
}
return $oRule;
}
public function setValue($mValue) {
$this->mValue = $mValue;
}
/**
* @param string $sRule
*
* @return array<int, string>
*/
private static function listDelimiterForRule($sRule)
{
if (preg_match('/^font($|-)/', $sRule)) {
return [',', '/', ' '];
}
return [',', ' ', '/'];
}
/**
* @deprecated Old-Style 2-dimensional array given. Retained for (some) backwards-compatibility. Use setValue() instead and wrapp the value inside a RuleValueList if necessary.
*/
public function setValues($aSpaceSeparatedValues) {
$oSpaceSeparatedList = null;
if (count($aSpaceSeparatedValues) > 1) {
$oSpaceSeparatedList = new RuleValueList(' ', $this->iLineNo);
}
foreach ($aSpaceSeparatedValues as $aCommaSeparatedValues) {
$oCommaSeparatedList = null;
if (count($aCommaSeparatedValues) > 1) {
$oCommaSeparatedList = new RuleValueList(',', $this->iLineNo);
}
foreach ($aCommaSeparatedValues as $mValue) {
if (!$oSpaceSeparatedList && !$oCommaSeparatedList) {
$this->mValue = $mValue;
return $mValue;
}
if ($oCommaSeparatedList) {
$oCommaSeparatedList->addListComponent($mValue);
} else {
$oSpaceSeparatedList->addListComponent($mValue);
}
}
if (!$oSpaceSeparatedList) {
$this->mValue = $oCommaSeparatedList;
return $oCommaSeparatedList;
} else {
$oSpaceSeparatedList->addListComponent($oCommaSeparatedList);
}
}
$this->mValue = $oSpaceSeparatedList;
return $oSpaceSeparatedList;
}
/**
* @return int
*/
public function getLineNo()
{
return $this->iLineNo;
}
/**
* @deprecated Old-Style 2-dimensional array returned. Retained for (some) backwards-compatibility. Use getValue() instead and check for the existance of a (nested set of) ValueList object(s).
*/
public function getValues() {
if (!$this->mValue instanceof RuleValueList) {
return array(array($this->mValue));
}
if ($this->mValue->getListSeparator() === ',') {
return array($this->mValue->getListComponents());
}
$aResult = array();
foreach ($this->mValue->getListComponents() as $mValue) {
if (!$mValue instanceof RuleValueList || $mValue->getListSeparator() !== ',') {
$aResult[] = array($mValue);
continue;
}
if ($this->mValue->getListSeparator() === ' ' || count($aResult) === 0) {
$aResult[] = array();
}
foreach ($mValue->getListComponents() as $mValue) {
$aResult[count($aResult) - 1][] = $mValue;
}
}
return $aResult;
}
/**
* @return int
*/
public function getColNo()
{
return $this->iColNo;
}
/**
* Adds a value to the existing value. Value will be appended if a RuleValueList exists of the given type. Otherwise, the existing value will be wrapped by one.
*/
public function addValue($mValue, $sType = ' ') {
if (!is_array($mValue)) {
$mValue = array($mValue);
}
if (!$this->mValue instanceof RuleValueList || $this->mValue->getListSeparator() !== $sType) {
$mCurrentValue = $this->mValue;
$this->mValue = new RuleValueList($sType, $this->iLineNo);
if ($mCurrentValue) {
$this->mValue->addListComponent($mCurrentValue);
}
}
foreach ($mValue as $mValueItem) {
$this->mValue->addListComponent($mValueItem);
}
}
/**
* @param int $iLine
* @param int $iColumn
*
* @return void
*/
public function setPosition($iLine, $iColumn)
{
$this->iColNo = $iColumn;
$this->iLineNo = $iLine;
}
public function addIeHack($iModifier) {
$this->aIeHack[] = $iModifier;
}
/**
* @param string $sRule
*
* @return void
*/
public function setRule($sRule)
{
$this->sRule = $sRule;
}
public function setIeHack(array $aModifiers) {
$this->aIeHack = $aModifiers;
}
/**
* @return string
*/
public function getRule()
{
return $this->sRule;
}
public function getIeHack() {
return $this->aIeHack;
}
/**
* @return RuleValueList|null
*/
public function getValue()
{
return $this->mValue;
}
public function setIsImportant($bIsImportant) {
$this->bIsImportant = $bIsImportant;
}
/**
* @param RuleValueList|null $mValue
*
* @return void
*/
public function setValue($mValue)
{
$this->mValue = $mValue;
}
public function getIsImportant() {
return $this->bIsImportant;
}
/**
* @param array<array-key, array<array-key, RuleValueList>> $aSpaceSeparatedValues
*
* @return RuleValueList
*
* @deprecated will be removed in version 9.0
* Old-Style 2-dimensional array given. Retained for (some) backwards-compatibility.
* Use `setValue()` instead and wrap the value inside a RuleValueList if necessary.
*/
public function setValues(array $aSpaceSeparatedValues)
{
$oSpaceSeparatedList = null;
if (count($aSpaceSeparatedValues) > 1) {
$oSpaceSeparatedList = new RuleValueList(' ', $this->iLineNo);
}
foreach ($aSpaceSeparatedValues as $aCommaSeparatedValues) {
$oCommaSeparatedList = null;
if (count($aCommaSeparatedValues) > 1) {
$oCommaSeparatedList = new RuleValueList(',', $this->iLineNo);
}
foreach ($aCommaSeparatedValues as $mValue) {
if (!$oSpaceSeparatedList && !$oCommaSeparatedList) {
$this->mValue = $mValue;
return $mValue;
}
if ($oCommaSeparatedList) {
$oCommaSeparatedList->addListComponent($mValue);
} else {
$oSpaceSeparatedList->addListComponent($mValue);
}
}
if (!$oSpaceSeparatedList) {
$this->mValue = $oCommaSeparatedList;
return $oCommaSeparatedList;
} else {
$oSpaceSeparatedList->addListComponent($oCommaSeparatedList);
}
}
$this->mValue = $oSpaceSeparatedList;
return $oSpaceSeparatedList;
}
public function __toString() {
return $this->render(new \Sabberworm\CSS\OutputFormat());
}
/**
* @return array<int, array<int, RuleValueList>>
*
* @deprecated will be removed in version 9.0
* Old-Style 2-dimensional array returned. Retained for (some) backwards-compatibility.
* Use `getValue()` instead and check for the existence of a (nested set of) ValueList object(s).
*/
public function getValues()
{
if (!$this->mValue instanceof RuleValueList) {
return [[$this->mValue]];
}
if ($this->mValue->getListSeparator() === ',') {
return [$this->mValue->getListComponents()];
}
$aResult = [];
foreach ($this->mValue->getListComponents() as $mValue) {
if (!$mValue instanceof RuleValueList || $mValue->getListSeparator() !== ',') {
$aResult[] = [$mValue];
continue;
}
if ($this->mValue->getListSeparator() === ' ' || count($aResult) === 0) {
$aResult[] = [];
}
foreach ($mValue->getListComponents() as $mValue) {
$aResult[count($aResult) - 1][] = $mValue;
}
}
return $aResult;
}
public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) {
$sResult = "{$this->sRule}:{$oOutputFormat->spaceAfterRuleName()}";
if ($this->mValue instanceof Value) { //Can also be a ValueList
$sResult .= $this->mValue->render($oOutputFormat);
} else {
$sResult .= $this->mValue;
}
if (!empty($this->aIeHack)) {
$sResult .= ' \\' . implode('\\', $this->aIeHack);
}
if ($this->bIsImportant) {
$sResult .= ' !important';
}
$sResult .= ';';
return $sResult;
}
/**
* Adds a value to the existing value. Value will be appended if a `RuleValueList` exists of the given type.
* Otherwise, the existing value will be wrapped by one.
*
* @param RuleValueList|array<int, RuleValueList> $mValue
* @param string $sType
*
* @return void
*/
public function addValue($mValue, $sType = ' ')
{
if (!is_array($mValue)) {
$mValue = [$mValue];
}
if (!$this->mValue instanceof RuleValueList || $this->mValue->getListSeparator() !== $sType) {
$mCurrentValue = $this->mValue;
$this->mValue = new RuleValueList($sType, $this->iLineNo);
if ($mCurrentValue) {
$this->mValue->addListComponent($mCurrentValue);
}
}
foreach ($mValue as $mValueItem) {
$this->mValue->addListComponent($mValueItem);
}
}
/**
* @param array $aComments Array of comments.
*/
public function addComments(array $aComments) {
$this->aComments = array_merge($this->aComments, $aComments);
}
/**
* @param int $iModifier
*
* @return void
*/
public function addIeHack($iModifier)
{
$this->aIeHack[] = $iModifier;
}
/**
* @return array
*/
public function getComments() {
return $this->aComments;
}
/**
* @param array<int, int> $aModifiers
*
* @return void
*/
public function setIeHack(array $aModifiers)
{
$this->aIeHack = $aModifiers;
}
/**
* @param array $aComments Array containing Comment objects.
*/
public function setComments(array $aComments) {
$this->aComments = $aComments;
}
/**
* @return array<int, int>
*/
public function getIeHack()
{
return $this->aIeHack;
}
/**
* @param bool $bIsImportant
*
* @return void
*/
public function setIsImportant($bIsImportant)
{
$this->bIsImportant = $bIsImportant;
}
/**
* @return bool
*/
public function getIsImportant()
{
return $this->bIsImportant;
}
/**
* @return string
*/
public function __toString()
{
return $this->render(new OutputFormat());
}
/**
* @return string
*/
public function render(OutputFormat $oOutputFormat)
{
$sResult = "{$this->sRule}:{$oOutputFormat->spaceAfterRuleName()}";
if ($this->mValue instanceof Value) { //Can also be a ValueList
$sResult .= $this->mValue->render($oOutputFormat);
} else {
$sResult .= $this->mValue;
}
if (!empty($this->aIeHack)) {
$sResult .= ' \\' . implode('\\', $this->aIeHack);
}
if ($this->bIsImportant) {
$sResult .= ' !important';
}
$sResult .= ';';
return $sResult;
}
/**
* @param array<array-key, Comment> $aComments
*
* @return void
*/
public function addComments(array $aComments)
{
$this->aComments = array_merge($this->aComments, $aComments);
}
/**
* @return array<array-key, Comment>
*/
public function getComments()
{
return $this->aComments;
}
/**
* @param array<array-key, Comment> $aComments
*
* @return void
*/
public function setComments(array $aComments)
{
$this->aComments = $aComments;
}
}

View File

@ -2,43 +2,72 @@
namespace Sabberworm\CSS\RuleSet;
use Sabberworm\CSS\OutputFormat;
use Sabberworm\CSS\Property\AtRule;
/**
* A RuleSet constructed by an unknown @-rule. @font-face rules are rendered into AtRuleSet objects.
* A RuleSet constructed by an unknown at-rule. `@font-face` rules are rendered into AtRuleSet objects.
*/
class AtRuleSet extends RuleSet implements AtRule {
class AtRuleSet extends RuleSet implements AtRule
{
/**
* @var string
*/
private $sType;
private $sType;
private $sArgs;
/**
* @var string
*/
private $sArgs;
public function __construct($sType, $sArgs = '', $iLineNo = 0) {
parent::__construct($iLineNo);
$this->sType = $sType;
$this->sArgs = $sArgs;
}
/**
* @param string $sType
* @param string $sArgs
* @param int $iLineNo
*/
public function __construct($sType, $sArgs = '', $iLineNo = 0)
{
parent::__construct($iLineNo);
$this->sType = $sType;
$this->sArgs = $sArgs;
}
public function atRuleName() {
return $this->sType;
}
/**
* @return string
*/
public function atRuleName()
{
return $this->sType;
}
public function atRuleArgs() {
return $this->sArgs;
}
/**
* @return string
*/
public function atRuleArgs()
{
return $this->sArgs;
}
public function __toString() {
return $this->render(new \Sabberworm\CSS\OutputFormat());
}
/**
* @return string
*/
public function __toString()
{
return $this->render(new OutputFormat());
}
public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) {
$sArgs = $this->sArgs;
if($sArgs) {
$sArgs = ' ' . $sArgs;
}
$sResult = "@{$this->sType}$sArgs{$oOutputFormat->spaceBeforeOpeningBrace()}{";
$sResult .= parent::render($oOutputFormat);
$sResult .= '}';
return $sResult;
}
}
/**
* @return string
*/
public function render(OutputFormat $oOutputFormat)
{
$sArgs = $this->sArgs;
if ($sArgs) {
$sArgs = ' ' . $sArgs;
}
$sResult = "@{$this->sType}$sArgs{$oOutputFormat->spaceBeforeOpeningBrace()}{";
$sResult .= parent::render($oOutputFormat);
$sResult .= '}';
return $sResult;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -2,211 +2,325 @@
namespace Sabberworm\CSS\RuleSet;
use Sabberworm\CSS\Comment\Comment;
use Sabberworm\CSS\Comment\Commentable;
use Sabberworm\CSS\OutputFormat;
use Sabberworm\CSS\Parsing\ParserState;
use Sabberworm\CSS\Parsing\UnexpectedEOFException;
use Sabberworm\CSS\Parsing\UnexpectedTokenException;
use Sabberworm\CSS\Renderable;
use Sabberworm\CSS\Rule\Rule;
/**
* RuleSet is a generic superclass denoting rules. The typical example for rule sets are declaration block.
* However, unknown At-Rules (like @font-face) are also rule sets.
* However, unknown At-Rules (like `@font-face`) are also rule sets.
*/
abstract class RuleSet implements Renderable, Commentable {
abstract class RuleSet implements Renderable, Commentable
{
/**
* @var array<string, Rule>
*/
private $aRules;
private $aRules;
protected $iLineNo;
protected $aComments;
/**
* @var int
*/
protected $iLineNo;
public function __construct($iLineNo = 0) {
$this->aRules = array();
$this->iLineNo = $iLineNo;
$this->aComments = array();
}
/**
* @var array<array-key, Comment>
*/
protected $aComments;
public static function parseRuleSet(ParserState $oParserState, RuleSet $oRuleSet) {
while ($oParserState->comes(';')) {
$oParserState->consume(';');
}
while (!$oParserState->comes('}')) {
$oRule = null;
if($oParserState->getSettings()->bLenientParsing) {
try {
$oRule = Rule::parse($oParserState);
} catch (UnexpectedTokenException $e) {
try {
$sConsume = $oParserState->consumeUntil(array("\n", ";", '}'), true);
// We need to “unfind” the matches to the end of the ruleSet as this will be matched later
if($oParserState->streql(substr($sConsume, -1), '}')) {
$oParserState->backtrack(1);
} else {
while ($oParserState->comes(';')) {
$oParserState->consume(';');
}
}
} catch (UnexpectedTokenException $e) {
// Weve reached the end of the document. Just close the RuleSet.
return;
}
}
} else {
$oRule = Rule::parse($oParserState);
}
if($oRule) {
$oRuleSet->addRule($oRule);
}
}
$oParserState->consume('}');
}
/**
* @param int $iLineNo
*/
public function __construct($iLineNo = 0)
{
$this->aRules = [];
$this->iLineNo = $iLineNo;
$this->aComments = [];
}
/**
* @return int
*/
public function getLineNo() {
return $this->iLineNo;
}
/**
* @return void
*
* @throws UnexpectedTokenException
* @throws UnexpectedEOFException
*/
public static function parseRuleSet(ParserState $oParserState, RuleSet $oRuleSet)
{
while ($oParserState->comes(';')) {
$oParserState->consume(';');
}
while (!$oParserState->comes('}')) {
$oRule = null;
if ($oParserState->getSettings()->bLenientParsing) {
try {
$oRule = Rule::parse($oParserState);
} catch (UnexpectedTokenException $e) {
try {
$sConsume = $oParserState->consumeUntil(["\n", ";", '}'], true);
// We need to “unfind” the matches to the end of the ruleSet as this will be matched later
if ($oParserState->streql(substr($sConsume, -1), '}')) {
$oParserState->backtrack(1);
} else {
while ($oParserState->comes(';')) {
$oParserState->consume(';');
}
}
} catch (UnexpectedTokenException $e) {
// 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 addRule(Rule $oRule, Rule $oSibling = null) {
$sRule = $oRule->getRule();
if(!isset($this->aRules[$sRule])) {
$this->aRules[$sRule] = array();
}
/**
* @return int
*/
public function getLineNo()
{
return $this->iLineNo;
}
$iPosition = count($this->aRules[$sRule]);
/**
* @param Rule|null $oSibling
*
* @return void
*/
public function addRule(Rule $oRule, Rule $oSibling = null)
{
$sRule = $oRule->getRule();
if (!isset($this->aRules[$sRule])) {
$this->aRules[$sRule] = [];
}
if ($oSibling !== null) {
$iSiblingPos = array_search($oSibling, $this->aRules[$sRule], true);
if ($iSiblingPos !== false) {
$iPosition = $iSiblingPos;
}
}
$iPosition = count($this->aRules[$sRule]);
array_splice($this->aRules[$sRule], $iPosition, 0, array($oRule));
}
if ($oSibling !== null) {
$iSiblingPos = array_search($oSibling, $this->aRules[$sRule], true);
if ($iSiblingPos !== false) {
$iPosition = $iSiblingPos;
$oRule->setPosition($oSibling->getLineNo(), $oSibling->getColNo() - 1);
}
}
if ($oRule->getLineNo() === 0 && $oRule->getColNo() === 0) {
//this node is added manually, give it the next best line
$rules = $this->getRules();
$pos = count($rules);
if ($pos > 0) {
$last = $rules[$pos - 1];
$oRule->setPosition($last->getLineNo() + 1, 0);
}
}
/**
* Returns all rules matching the given rule name
* @param (null|string|Rule) $mRule pattern to search for. If null, returns all rules. if the pattern ends with a dash, all rules starting with the pattern are returned as well as one matching the pattern with the dash excluded. passing a Rule behaves like calling getRules($mRule->getRule()).
* @example $oRuleSet->getRules('font-') //returns an array of all rules either beginning with font- or matching font.
* @example $oRuleSet->getRules('font') //returns array(0 => $oRule, …) or array().
* @return Rule[] Rules.
*/
public function getRules($mRule = null) {
if ($mRule instanceof Rule) {
$mRule = $mRule->getRule();
}
$aResult = array();
foreach($this->aRules as $sName => $aRules) {
// Either no search rule is given or the search rule matches the found rule exactly or the search rule ends in “-” and the found rule starts with the search rule.
if(!$mRule || $sName === $mRule || (strrpos($mRule, '-') === strlen($mRule) - strlen('-') && (strpos($sName, $mRule) === 0 || $sName === substr($mRule, 0, -1)))) {
$aResult = array_merge($aResult, $aRules);
}
}
return $aResult;
}
array_splice($this->aRules[$sRule], $iPosition, 0, [$oRule]);
}
/**
* Override all the rules of this set.
* @param Rule[] $aRules The rules to override with.
*/
public function setRules(array $aRules) {
$this->aRules = array();
foreach ($aRules as $rule) {
$this->addRule($rule);
}
}
/**
* Returns all rules matching the given rule name
*
* @example $oRuleSet->getRules('font') // returns array(0 => $oRule, …) or array().
*
* @example $oRuleSet->getRules('font-')
* //returns an array of all rules either beginning with font- or matching font.
*
* @param Rule|string|null $mRule
* Pattern to search for. If null, returns all rules.
* If the pattern ends with a dash, all rules starting with the pattern are returned
* as well as one matching the pattern with the dash excluded.
* Passing a Rule behaves like calling `getRules($mRule->getRule())`.
*
* @return array<int, Rule>
*/
public function getRules($mRule = null)
{
if ($mRule instanceof Rule) {
$mRule = $mRule->getRule();
}
/** @var array<int, Rule> $aResult */
$aResult = [];
foreach ($this->aRules as $sName => $aRules) {
// Either no search rule is given or the search rule matches the found rule exactly
// or the search rule ends in “-” and the found rule starts with the search rule.
if (
!$mRule || $sName === $mRule
|| (
strrpos($mRule, '-') === strlen($mRule) - strlen('-')
&& (strpos($sName, $mRule) === 0 || $sName === substr($mRule, 0, -1))
)
) {
$aResult = array_merge($aResult, $aRules);
}
}
usort($aResult, function (Rule $first, Rule $second) {
if ($first->getLineNo() === $second->getLineNo()) {
return $first->getColNo() - $second->getColNo();
}
return $first->getLineNo() - $second->getLineNo();
});
return $aResult;
}
/**
* Returns all rules matching the given pattern and returns them in an associative array with the rules name as keys. This method exists mainly for backwards-compatibility and is really only partially useful.
* @param (string) $mRule pattern to search for. If null, returns all rules. if the pattern ends with a dash, all rules starting with the pattern are returned as well as one matching the pattern with the dash excluded. passing a Rule behaves like calling getRules($mRule->getRule()).
* Note: This method loses some information: Calling this (with an argument of 'background-') on a declaration block like { background-color: green; background-color; rgba(0, 127, 0, 0.7); } will only yield an associative array containing the rgba-valued rule while @link{getRules()} would yield an indexed array containing both.
* @return Rule[] Rules.
*/
public function getRulesAssoc($mRule = null) {
$aResult = array();
foreach($this->getRules($mRule) as $oRule) {
$aResult[$oRule->getRule()] = $oRule;
}
return $aResult;
}
/**
* Overrides all the rules of this set.
*
* @param array<array-key, Rule> $aRules The rules to override with.
*
* @return void
*/
public function setRules(array $aRules)
{
$this->aRules = [];
foreach ($aRules as $rule) {
$this->addRule($rule);
}
}
/**
* Remove a rule from this RuleSet. This accepts all the possible values that @link{getRules()} accepts. If given a Rule, it will only remove this particular rule (by identity). If given a name, it will remove all rules by that name. Note: this is different from pre-v.2.0 behaviour of PHP-CSS-Parser, where passing a Rule instance would remove all rules with the same name. To get the old behvaiour, use removeRule($oRule->getRule()).
* @param (null|string|Rule) $mRule pattern to remove. If $mRule is null, all rules are removed. If the pattern ends in a dash, all rules starting with the pattern are removed as well as one matching the pattern with the dash excluded. Passing a Rule behaves matches by identity.
*/
public function removeRule($mRule) {
if($mRule instanceof Rule) {
$sRule = $mRule->getRule();
if(!isset($this->aRules[$sRule])) {
return;
}
foreach($this->aRules[$sRule] as $iKey => $oRule) {
if($oRule === $mRule) {
unset($this->aRules[$sRule][$iKey]);
}
}
} else {
foreach($this->aRules as $sName => $aRules) {
// Either no search rule is given or the search rule matches the found rule exactly or the search rule ends in “-” and the found rule starts with the search rule or equals it (without the trailing dash).
if(!$mRule || $sName === $mRule || (strrpos($mRule, '-') === strlen($mRule) - strlen('-') && (strpos($sName, $mRule) === 0 || $sName === substr($mRule, 0, -1)))) {
unset($this->aRules[$sName]);
}
}
}
}
/**
* Returns all rules matching the given pattern and returns them in an associative array with the rules name
* as keys. This method exists mainly for backwards-compatibility and is really only partially useful.
*
* Note: This method loses some information: Calling this (with an argument of `background-`) on a declaration block
* like `{ background-color: green; background-color; rgba(0, 127, 0, 0.7); }` will only yield an associative array
* containing the rgba-valued rule while `getRules()` would yield an indexed array containing both.
*
* @param Rule|string|null $mRule $mRule
* Pattern to search for. If null, returns all rules. If the pattern ends with a dash,
* all rules starting with the pattern are returned as well as one matching the pattern with the dash
* excluded. Passing a Rule behaves like calling `getRules($mRule->getRule())`.
*
* @return array<string, Rule>
*/
public function getRulesAssoc($mRule = null)
{
/** @var array<string, Rule> $aResult */
$aResult = [];
foreach ($this->getRules($mRule) as $oRule) {
$aResult[$oRule->getRule()] = $oRule;
}
return $aResult;
}
public function __toString() {
return $this->render(new \Sabberworm\CSS\OutputFormat());
}
/**
* Removes a rule from this RuleSet. This accepts all the possible values that `getRules()` accepts.
*
* If given a Rule, it will only remove this particular rule (by identity).
* If given a name, it will remove all rules by that name.
*
* Note: this is different from pre-v.2.0 behaviour of PHP-CSS-Parser, where passing a Rule instance would
* remove all rules with the same name. To get the old behaviour, use `removeRule($oRule->getRule())`.
*
* @param Rule|string|null $mRule
* pattern to remove. If $mRule is null, all rules are removed. If the pattern ends in a dash,
* all rules starting with the pattern are removed as well as one matching the pattern with the dash
* excluded. Passing a Rule behaves matches by identity.
*
* @return void
*/
public function removeRule($mRule)
{
if ($mRule instanceof Rule) {
$sRule = $mRule->getRule();
if (!isset($this->aRules[$sRule])) {
return;
}
foreach ($this->aRules[$sRule] as $iKey => $oRule) {
if ($oRule === $mRule) {
unset($this->aRules[$sRule][$iKey]);
}
}
} else {
foreach ($this->aRules as $sName => $aRules) {
// Either no search rule is given or the search rule matches the found rule exactly
// or the search rule ends in “-” and the found rule starts with the search rule or equals it
// (without the trailing dash).
if (
!$mRule || $sName === $mRule
|| (strrpos($mRule, '-') === strlen($mRule) - strlen('-')
&& (strpos($sName, $mRule) === 0 || $sName === substr($mRule, 0, -1)))
) {
unset($this->aRules[$sName]);
}
}
}
}
public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) {
$sResult = '';
$bIsFirst = true;
foreach ($this->aRules as $aRules) {
foreach($aRules as $oRule) {
$sRendered = $oOutputFormat->safely(function() use ($oRule, $oOutputFormat) {
return $oRule->render($oOutputFormat->nextLevel());
});
if($sRendered === null) {
continue;
}
if($bIsFirst) {
$bIsFirst = false;
$sResult .= $oOutputFormat->nextLevel()->spaceBeforeRules();
} else {
$sResult .= $oOutputFormat->nextLevel()->spaceBetweenRules();
}
$sResult .= $sRendered;
}
}
if(!$bIsFirst) {
// Had some output
$sResult .= $oOutputFormat->spaceAfterRules();
}
/**
* @return string
*/
public function __toString()
{
return $this->render(new OutputFormat());
}
return $oOutputFormat->removeLastSemicolon($sResult);
}
/**
* @return string
*/
public function render(OutputFormat $oOutputFormat)
{
$sResult = '';
$bIsFirst = true;
foreach ($this->aRules as $aRules) {
foreach ($aRules as $oRule) {
$sRendered = $oOutputFormat->safely(function () use ($oRule, $oOutputFormat) {
return $oRule->render($oOutputFormat->nextLevel());
});
if ($sRendered === null) {
continue;
}
if ($bIsFirst) {
$bIsFirst = false;
$sResult .= $oOutputFormat->nextLevel()->spaceBeforeRules();
} else {
$sResult .= $oOutputFormat->nextLevel()->spaceBetweenRules();
}
$sResult .= $sRendered;
}
}
/**
* @param array $aComments Array of comments.
*/
public function addComments(array $aComments) {
$this->aComments = array_merge($this->aComments, $aComments);
}
if (!$bIsFirst) {
// Had some output
$sResult .= $oOutputFormat->spaceAfterRules();
}
/**
* @return array
*/
public function getComments() {
return $this->aComments;
}
return $oOutputFormat->removeLastSemicolon($sResult);
}
/**
* @param array $aComments Array containing Comment objects.
*/
public function setComments(array $aComments) {
$this->aComments = $aComments;
}
/**
* @param array<string, Comment> $aComments
*
* @return void
*/
public function addComments(array $aComments)
{
$this->aComments = array_merge($this->aComments, $aComments);
}
/**
* @return array<string, Comment>
*/
public function getComments()
{
return $this->aComments;
}
/**
* @param array<string, Comment> $aComments
*
* @return void
*/
public function setComments(array $aComments)
{
$this->aComments = $aComments;
}
}

View File

@ -2,53 +2,88 @@
namespace Sabberworm\CSS;
use Sabberworm\CSS\Rule\Rule;
/**
* Parser settings class.
*
* Configure parser behaviour here.
*/
class Settings {
/**
* Multi-byte string support. If true (mbstring extension must be enabled), will use (slower) mb_strlen, mb_convert_case, mb_substr and mb_strpos functions. Otherwise, the normal (ASCII-Only) functions will be used.
*/
public $bMultibyteSupport;
class Settings
{
/**
* Multi-byte string support.
* If true (mbstring extension must be enabled), will use (slower) `mb_strlen`, `mb_convert_case`, `mb_substr`
* and `mb_strpos` functions. Otherwise, the normal (ASCII-Only) functions will be used.
*
* @var bool
*/
public $bMultibyteSupport;
/**
* The default charset for the CSS if no `@charset` rule is found. Defaults to utf-8.
*/
public $sDefaultCharset = 'utf-8';
/**
* The default charset for the CSS if no `@charset` rule is found. Defaults to utf-8.
*
* @var string
*/
public $sDefaultCharset = 'utf-8';
/**
* Lenient parsing. When used (which is true by default), the parser will not choke on unexpected tokens but simply ignore them.
*/
public $bLenientParsing = true;
/**
* Lenient parsing. When used (which is true by default), the parser will not choke
* on unexpected tokens but simply ignore them.
*
* @var bool
*/
public $bLenientParsing = true;
private function __construct() {
$this->bMultibyteSupport = extension_loaded('mbstring');
}
private function __construct()
{
$this->bMultibyteSupport = extension_loaded('mbstring');
}
public static function create() {
return new Settings();
}
public function withMultibyteSupport($bMultibyteSupport = true) {
$this->bMultibyteSupport = $bMultibyteSupport;
return $this;
}
public function withDefaultCharset($sDefaultCharset) {
$this->sDefaultCharset = $sDefaultCharset;
return $this;
}
public function withLenientParsing($bLenientParsing = true) {
$this->bLenientParsing = $bLenientParsing;
return $this;
}
public function beStrict() {
return $this->withLenientParsing(false);
}
}
/**
* @return self new instance
*/
public static function create()
{
return new Settings();
}
/**
* @param bool $bMultibyteSupport
*
* @return self fluent interface
*/
public function withMultibyteSupport($bMultibyteSupport = true)
{
$this->bMultibyteSupport = $bMultibyteSupport;
return $this;
}
/**
* @param string $sDefaultCharset
*
* @return self fluent interface
*/
public function withDefaultCharset($sDefaultCharset)
{
$this->sDefaultCharset = $sDefaultCharset;
return $this;
}
/**
* @param bool $bLenientParsing
*
* @return self fluent interface
*/
public function withLenientParsing($bLenientParsing = true)
{
$this->bLenientParsing = $bLenientParsing;
return $this;
}
/**
* @return self fluent interface
*/
public function beStrict()
{
return $this->withLenientParsing(false);
}
}

View File

@ -2,39 +2,72 @@
namespace Sabberworm\CSS\Value;
class CSSFunction extends ValueList {
use Sabberworm\CSS\OutputFormat;
protected $sName;
class CSSFunction extends ValueList
{
/**
* @var string
*/
protected $sName;
public function __construct($sName, $aArguments, $sSeparator = ',', $iLineNo = 0) {
if($aArguments instanceof RuleValueList) {
$sSeparator = $aArguments->getListSeparator();
$aArguments = $aArguments->getListComponents();
}
$this->sName = $sName;
$this->iLineNo = $iLineNo;
parent::__construct($aArguments, $sSeparator, $iLineNo);
}
/**
* @param string $sName
* @param RuleValueList|array<int, RuleValueList|CSSFunction|CSSString|LineName|Size|URL|string> $aArguments
* @param string $sSeparator
* @param int $iLineNo
*/
public function __construct($sName, $aArguments, $sSeparator = ',', $iLineNo = 0)
{
if ($aArguments instanceof RuleValueList) {
$sSeparator = $aArguments->getListSeparator();
$aArguments = $aArguments->getListComponents();
}
$this->sName = $sName;
$this->iLineNo = $iLineNo;
parent::__construct($aArguments, $sSeparator, $iLineNo);
}
public function getName() {
return $this->sName;
}
/**
* @return string
*/
public function getName()
{
return $this->sName;
}
public function setName($sName) {
$this->sName = $sName;
}
/**
* @param string $sName
*
* @return void
*/
public function setName($sName)
{
$this->sName = $sName;
}
public function getArguments() {
return $this->aComponents;
}
/**
* @return array<int, RuleValueList|CSSFunction|CSSString|LineName|Size|URL|string>
*/
public function getArguments()
{
return $this->aComponents;
}
public function __toString() {
return $this->render(new \Sabberworm\CSS\OutputFormat());
}
public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) {
$aArguments = parent::render($oOutputFormat);
return "{$this->sName}({$aArguments})";
}
/**
* @return string
*/
public function __toString()
{
return $this->render(new OutputFormat());
}
/**
* @return string
*/
public function render(OutputFormat $oOutputFormat)
{
$aArguments = parent::render($oOutputFormat);
return "{$this->sName}({$aArguments})";
}
}

View File

@ -2,65 +2,104 @@
namespace Sabberworm\CSS\Value;
use Sabberworm\CSS\OutputFormat;
use Sabberworm\CSS\Parsing\ParserState;
use Sabberworm\CSS\Parsing\SourceException;
use Sabberworm\CSS\Parsing\UnexpectedEOFException;
use Sabberworm\CSS\Parsing\UnexpectedTokenException;
class CSSString extends PrimitiveValue {
class CSSString extends PrimitiveValue
{
/**
* @var string
*/
private $sString;
private $sString;
/**
* @param string $sString
* @param int $iLineNo
*/
public function __construct($sString, $iLineNo = 0)
{
$this->sString = $sString;
parent::__construct($iLineNo);
}
public function __construct($sString, $iLineNo = 0) {
$this->sString = $sString;
parent::__construct($iLineNo);
}
/**
* @return CSSString
*
* @throws SourceException
* @throws UnexpectedEOFException
* @throws UnexpectedTokenException
*/
public static function parse(ParserState $oParserState)
{
$sBegin = $oParserState->peek();
$sQuote = null;
if ($sBegin === "'") {
$sQuote = "'";
} elseif ($sBegin === '"') {
$sQuote = '"';
}
if ($sQuote !== null) {
$oParserState->consume($sQuote);
}
$sResult = "";
$sContent = null;
if ($sQuote === null) {
// Unquoted strings end in whitespace or with braces, brackets, parentheses
while (!preg_match('/[\\s{}()<>\\[\\]]/isu', $oParserState->peek())) {
$sResult .= $oParserState->parseCharacter(false);
}
} else {
while (!$oParserState->comes($sQuote)) {
$sContent = $oParserState->parseCharacter(false);
if ($sContent === null) {
throw new SourceException(
"Non-well-formed quoted string {$oParserState->peek(3)}",
$oParserState->currentLine()
);
}
$sResult .= $sContent;
}
$oParserState->consume($sQuote);
}
return new CSSString($sResult, $oParserState->currentLine());
}
public static function parse(ParserState $oParserState) {
$sBegin = $oParserState->peek();
$sQuote = null;
if ($sBegin === "'") {
$sQuote = "'";
} else if ($sBegin === '"') {
$sQuote = '"';
}
if ($sQuote !== null) {
$oParserState->consume($sQuote);
}
$sResult = "";
$sContent = null;
if ($sQuote === null) {
// Unquoted strings end in whitespace or with braces, brackets, parentheses
while (!preg_match('/[\\s{}()<>\\[\\]]/isu', $oParserState->peek())) {
$sResult .= $oParserState->parseCharacter(false);
}
} else {
while (!$oParserState->comes($sQuote)) {
$sContent = $oParserState->parseCharacter(false);
if ($sContent === null) {
throw new SourceException("Non-well-formed quoted string {$oParserState->peek(3)}", $oParserState->currentLine());
}
$sResult .= $sContent;
}
$oParserState->consume($sQuote);
}
return new CSSString($sResult, $oParserState->currentLine());
}
/**
* @param string $sString
*
* @return void
*/
public function setString($sString)
{
$this->sString = $sString;
}
public function setString($sString) {
$this->sString = $sString;
}
/**
* @return string
*/
public function getString()
{
return $this->sString;
}
public function getString() {
return $this->sString;
}
/**
* @return string
*/
public function __toString()
{
return $this->render(new OutputFormat());
}
public function __toString() {
return $this->render(new \Sabberworm\CSS\OutputFormat());
}
public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) {
$sString = addslashes($this->sString);
$sString = str_replace("\n", '\A', $sString);
return $oOutputFormat->getStringQuotingType() . $sString . $oOutputFormat->getStringQuotingType();
}
}
/**
* @return string
*/
public function render(OutputFormat $oOutputFormat)
{
$sString = addslashes($this->sString);
$sString = str_replace("\n", '\A', $sString);
return $oOutputFormat->getStringQuotingType() . $sString . $oOutputFormat->getStringQuotingType();
}
}

View File

@ -3,60 +3,87 @@
namespace Sabberworm\CSS\Value;
use Sabberworm\CSS\Parsing\ParserState;
use Sabberworm\CSS\Parsing\UnexpectedEOFException;
use Sabberworm\CSS\Parsing\UnexpectedTokenException;
class CalcFunction extends CSSFunction {
const T_OPERAND = 1;
const T_OPERATOR = 2;
class CalcFunction extends CSSFunction
{
/**
* @var int
*/
const T_OPERAND = 1;
public static function parse(ParserState $oParserState) {
$aOperators = array('+', '-', '*', '/');
$sFunction = trim($oParserState->consumeUntil('(', false, true));
$oCalcList = new CalcRuleValueList($oParserState->currentLine());
$oList = new RuleValueList(',', $oParserState->currentLine());
$iNestingLevel = 0;
$iLastComponentType = NULL;
while(!$oParserState->comes(')') || $iNestingLevel > 0) {
$oParserState->consumeWhiteSpace();
if ($oParserState->comes('(')) {
$iNestingLevel++;
$oCalcList->addListComponent($oParserState->consume(1));
continue;
} else if ($oParserState->comes(')')) {
$iNestingLevel--;
$oCalcList->addListComponent($oParserState->consume(1));
continue;
}
if ($iLastComponentType != CalcFunction::T_OPERAND) {
$oVal = Value::parsePrimitiveValue($oParserState);
$oCalcList->addListComponent($oVal);
$iLastComponentType = CalcFunction::T_OPERAND;
} else {
if (in_array($oParserState->peek(), $aOperators)) {
if (($oParserState->comes('-') || $oParserState->comes('+'))) {
if ($oParserState->peek(1, -1) != ' ' || !($oParserState->comes('- ') || $oParserState->comes('+ '))) {
throw new UnexpectedTokenException(" {$oParserState->peek()} ", $oParserState->peek(1, -1) . $oParserState->peek(2), 'literal', $oParserState->currentLine());
}
}
$oCalcList->addListComponent($oParserState->consume(1));
$iLastComponentType = CalcFunction::T_OPERATOR;
} else {
throw new UnexpectedTokenException(
sprintf(
'Next token was expected to be an operand of type %s. Instead "%s" was found.',
implode(', ', $aOperators),
$oVal
),
'',
'custom',
$oParserState->currentLine()
);
}
}
}
$oList->addListComponent($oCalcList);
$oParserState->consume(')');
return new CalcFunction($sFunction, $oList, ',', $oParserState->currentLine());
}
/**
* @var int
*/
const T_OPERATOR = 2;
/**
* @return CalcFunction
*
* @throws UnexpectedTokenException
* @throws UnexpectedEOFException
*/
public static function parse(ParserState $oParserState)
{
$aOperators = ['+', '-', '*', '/'];
$sFunction = trim($oParserState->consumeUntil('(', false, true));
$oCalcList = new CalcRuleValueList($oParserState->currentLine());
$oList = new RuleValueList(',', $oParserState->currentLine());
$iNestingLevel = 0;
$iLastComponentType = null;
while (!$oParserState->comes(')') || $iNestingLevel > 0) {
$oParserState->consumeWhiteSpace();
if ($oParserState->comes('(')) {
$iNestingLevel++;
$oCalcList->addListComponent($oParserState->consume(1));
$oParserState->consumeWhiteSpace();
continue;
} elseif ($oParserState->comes(')')) {
$iNestingLevel--;
$oCalcList->addListComponent($oParserState->consume(1));
$oParserState->consumeWhiteSpace();
continue;
}
if ($iLastComponentType != CalcFunction::T_OPERAND) {
$oVal = Value::parsePrimitiveValue($oParserState);
$oCalcList->addListComponent($oVal);
$iLastComponentType = CalcFunction::T_OPERAND;
} else {
if (in_array($oParserState->peek(), $aOperators)) {
if (($oParserState->comes('-') || $oParserState->comes('+'))) {
if (
$oParserState->peek(1, -1) != ' '
|| !($oParserState->comes('- ')
|| $oParserState->comes('+ '))
) {
throw new UnexpectedTokenException(
" {$oParserState->peek()} ",
$oParserState->peek(1, -1) . $oParserState->peek(2),
'literal',
$oParserState->currentLine()
);
}
}
$oCalcList->addListComponent($oParserState->consume(1));
$iLastComponentType = CalcFunction::T_OPERATOR;
} else {
throw new UnexpectedTokenException(
sprintf(
'Next token was expected to be an operand of type %s. Instead "%s" was found.',
implode(', ', $aOperators),
$oVal
),
'',
'custom',
$oParserState->currentLine()
);
}
}
$oParserState->consumeWhiteSpace();
}
$oList->addListComponent($oCalcList);
$oParserState->consume(')');
return new CalcFunction($sFunction, $oList, ',', $oParserState->currentLine());
}
}

View File

@ -2,13 +2,23 @@
namespace Sabberworm\CSS\Value;
class CalcRuleValueList extends RuleValueList {
public function __construct($iLineNo = 0) {
parent::__construct(array(), ',', $iLineNo);
}
use Sabberworm\CSS\OutputFormat;
public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) {
return $oOutputFormat->implode(' ', $this->aComponents);
}
class CalcRuleValueList extends RuleValueList
{
/**
* @param int $iLineNo
*/
public function __construct($iLineNo = 0)
{
parent::__construct(',', $iLineNo);
}
/**
* @return string
*/
public function render(OutputFormat $oOutputFormat)
{
return $oOutputFormat->implode(' ', $this->aComponents);
}
}

View File

@ -2,94 +2,165 @@
namespace Sabberworm\CSS\Value;
use Sabberworm\CSS\OutputFormat;
use Sabberworm\CSS\Parsing\ParserState;
use Sabberworm\CSS\Parsing\UnexpectedEOFException;
use Sabberworm\CSS\Parsing\UnexpectedTokenException;
class Color extends CSSFunction {
class Color extends CSSFunction
{
/**
* @param array<int, RuleValueList|CSSFunction|CSSString|LineName|Size|URL|string> $aColor
* @param int $iLineNo
*/
public function __construct(array $aColor, $iLineNo = 0)
{
parent::__construct(implode('', array_keys($aColor)), $aColor, ',', $iLineNo);
}
public function __construct($aColor, $iLineNo = 0) {
parent::__construct(implode('', array_keys($aColor)), $aColor, ',', $iLineNo);
}
/**
* @return Color|CSSFunction
*
* @throws UnexpectedEOFException
* @throws UnexpectedTokenException
*/
public static function parse(ParserState $oParserState)
{
$aColor = [];
if ($oParserState->comes('#')) {
$oParserState->consume('#');
$sValue = $oParserState->parseIdentifier(false);
if ($oParserState->strlen($sValue) === 3) {
$sValue = $sValue[0] . $sValue[0] . $sValue[1] . $sValue[1] . $sValue[2] . $sValue[2];
} elseif ($oParserState->strlen($sValue) === 4) {
$sValue = $sValue[0] . $sValue[0] . $sValue[1] . $sValue[1] . $sValue[2] . $sValue[2] . $sValue[3]
. $sValue[3];
}
public static function parse(ParserState $oParserState) {
$aColor = array();
if ($oParserState->comes('#')) {
$oParserState->consume('#');
$sValue = $oParserState->parseIdentifier(false);
if ($oParserState->strlen($sValue) === 3) {
$sValue = $sValue[0] . $sValue[0] . $sValue[1] . $sValue[1] . $sValue[2] . $sValue[2];
} else if ($oParserState->strlen($sValue) === 4) {
$sValue = $sValue[0] . $sValue[0] . $sValue[1] . $sValue[1] . $sValue[2] . $sValue[2] . $sValue[3] . $sValue[3];
}
if ($oParserState->strlen($sValue) === 8) {
$aColor = [
'r' => new Size(intval($sValue[0] . $sValue[1], 16), null, true, $oParserState->currentLine()),
'g' => new Size(intval($sValue[2] . $sValue[3], 16), null, true, $oParserState->currentLine()),
'b' => new Size(intval($sValue[4] . $sValue[5], 16), null, true, $oParserState->currentLine()),
'a' => new Size(
round(self::mapRange(intval($sValue[6] . $sValue[7], 16), 0, 255, 0, 1), 2),
null,
true,
$oParserState->currentLine()
),
];
} else {
$aColor = [
'r' => new Size(intval($sValue[0] . $sValue[1], 16), null, true, $oParserState->currentLine()),
'g' => new Size(intval($sValue[2] . $sValue[3], 16), null, true, $oParserState->currentLine()),
'b' => new Size(intval($sValue[4] . $sValue[5], 16), null, true, $oParserState->currentLine()),
];
}
} else {
$sColorMode = $oParserState->parseIdentifier(true);
$oParserState->consumeWhiteSpace();
$oParserState->consume('(');
if ($oParserState->strlen($sValue) === 8) {
$aColor = array(
'r' => new Size(intval($sValue[0] . $sValue[1], 16), null, true, $oParserState->currentLine()),
'g' => new Size(intval($sValue[2] . $sValue[3], 16), null, true, $oParserState->currentLine()),
'b' => new Size(intval($sValue[4] . $sValue[5], 16), null, true, $oParserState->currentLine()),
'a' => new Size(round(self::mapRange(intval($sValue[6] . $sValue[7], 16), 0, 255, 0, 1), 2), null, true, $oParserState->currentLine())
);
} else {
$aColor = array(
'r' => new Size(intval($sValue[0] . $sValue[1], 16), null, true, $oParserState->currentLine()),
'g' => new Size(intval($sValue[2] . $sValue[3], 16), null, true, $oParserState->currentLine()),
'b' => new Size(intval($sValue[4] . $sValue[5], 16), null, true, $oParserState->currentLine())
);
}
} else {
$sColorMode = $oParserState->parseIdentifier(true);
$oParserState->consumeWhiteSpace();
$oParserState->consume('(');
$iLength = $oParserState->strlen($sColorMode);
for ($i = 0; $i < $iLength; ++$i) {
$oParserState->consumeWhiteSpace();
$aColor[$sColorMode[$i]] = Size::parse($oParserState, true);
$oParserState->consumeWhiteSpace();
if ($i < ($iLength - 1)) {
$oParserState->consume(',');
}
}
$oParserState->consume(')');
}
return new Color($aColor, $oParserState->currentLine());
}
$bContainsVar = false;
$iLength = $oParserState->strlen($sColorMode);
for ($i = 0; $i < $iLength; ++$i) {
$oParserState->consumeWhiteSpace();
if ($oParserState->comes('var')) {
$aColor[$sColorMode[$i]] = CSSFunction::parseIdentifierOrFunction($oParserState);
$bContainsVar = true;
} else {
$aColor[$sColorMode[$i]] = Size::parse($oParserState, true);
}
private static function mapRange($fVal, $fFromMin, $fFromMax, $fToMin, $fToMax) {
$fFromRange = $fFromMax - $fFromMin;
$fToRange = $fToMax - $fToMin;
$fMultiplier = $fToRange / $fFromRange;
$fNewVal = $fVal - $fFromMin;
$fNewVal *= $fMultiplier;
return $fNewVal + $fToMin;
}
if ($bContainsVar && $oParserState->comes(')')) {
// With a var argument the function can have fewer arguments
break;
}
public function getColor() {
return $this->aComponents;
}
$oParserState->consumeWhiteSpace();
if ($i < ($iLength - 1)) {
$oParserState->consume(',');
}
}
$oParserState->consume(')');
public function setColor($aColor) {
$this->setName(implode('', array_keys($aColor)));
$this->aComponents = $aColor;
}
if ($bContainsVar) {
return new CSSFunction($sColorMode, array_values($aColor), ',', $oParserState->currentLine());
}
}
return new Color($aColor, $oParserState->currentLine());
}
public function getColorDescription() {
return $this->getName();
}
/**
* @param float $fVal
* @param float $fFromMin
* @param float $fFromMax
* @param float $fToMin
* @param float $fToMax
*
* @return float
*/
private static function mapRange($fVal, $fFromMin, $fFromMax, $fToMin, $fToMax)
{
$fFromRange = $fFromMax - $fFromMin;
$fToRange = $fToMax - $fToMin;
$fMultiplier = $fToRange / $fFromRange;
$fNewVal = $fVal - $fFromMin;
$fNewVal *= $fMultiplier;
return $fNewVal + $fToMin;
}
public function __toString() {
return $this->render(new \Sabberworm\CSS\OutputFormat());
}
/**
* @return array<int, RuleValueList|CSSFunction|CSSString|LineName|Size|URL|string>
*/
public function getColor()
{
return $this->aComponents;
}
public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) {
// Shorthand RGB color values
if($oOutputFormat->getRGBHashNotation() && implode('', array_keys($this->aComponents)) === 'rgb') {
$sResult = sprintf(
'%02x%02x%02x',
$this->aComponents['r']->getSize(),
$this->aComponents['g']->getSize(),
$this->aComponents['b']->getSize()
);
return '#'.(($sResult[0] == $sResult[1]) && ($sResult[2] == $sResult[3]) && ($sResult[4] == $sResult[5]) ? "$sResult[0]$sResult[2]$sResult[4]" : $sResult);
}
return parent::render($oOutputFormat);
}
/**
* @param array<int, RuleValueList|CSSFunction|CSSString|LineName|Size|URL|string> $aColor
*
* @return void
*/
public function setColor(array $aColor)
{
$this->setName(implode('', array_keys($aColor)));
$this->aComponents = $aColor;
}
/**
* @return string
*/
public function getColorDescription()
{
return $this->getName();
}
/**
* @return string
*/
public function __toString()
{
return $this->render(new OutputFormat());
}
/**
* @return string
*/
public function render(OutputFormat $oOutputFormat)
{
// Shorthand RGB color values
if ($oOutputFormat->getRGBHashNotation() && implode('', array_keys($this->aComponents)) === 'rgb') {
$sResult = sprintf(
'%02x%02x%02x',
$this->aComponents['r']->getSize(),
$this->aComponents['g']->getSize(),
$this->aComponents['b']->getSize()
);
return '#' . (($sResult[0] == $sResult[1]) && ($sResult[2] == $sResult[3]) && ($sResult[4] == $sResult[5])
? "$sResult[0]$sResult[2]$sResult[4]" : $sResult);
}
return parent::render($oOutputFormat);
}
}

View File

@ -2,40 +2,64 @@
namespace Sabberworm\CSS\Value;
use Sabberworm\CSS\OutputFormat;
use Sabberworm\CSS\Parsing\ParserState;
use Sabberworm\CSS\Parsing\UnexpectedEOFException;
use Sabberworm\CSS\Parsing\UnexpectedTokenException;
class LineName extends ValueList {
public function __construct($aComponents = array(), $iLineNo = 0) {
parent::__construct($aComponents, ' ', $iLineNo);
}
class LineName extends ValueList
{
/**
* @param array<int, RuleValueList|CSSFunction|CSSString|LineName|Size|URL|string> $aComponents
* @param int $iLineNo
*/
public function __construct(array $aComponents = [], $iLineNo = 0)
{
parent::__construct($aComponents, ' ', $iLineNo);
}
public static function parse(ParserState $oParserState) {
$oParserState->consume('[');
$oParserState->consumeWhiteSpace();
$aNames = array();
do {
if($oParserState->getSettings()->bLenientParsing) {
try {
$aNames[] = $oParserState->parseIdentifier();
} catch(UnexpectedTokenException $e) {}
} else {
$aNames[] = $oParserState->parseIdentifier();
}
$oParserState->consumeWhiteSpace();
} while (!$oParserState->comes(']'));
$oParserState->consume(']');
return new LineName($aNames, $oParserState->currentLine());
}
/**
* @return LineName
*
* @throws UnexpectedTokenException
* @throws UnexpectedEOFException
*/
public static function parse(ParserState $oParserState)
{
$oParserState->consume('[');
$oParserState->consumeWhiteSpace();
$aNames = [];
do {
if ($oParserState->getSettings()->bLenientParsing) {
try {
$aNames[] = $oParserState->parseIdentifier();
} catch (UnexpectedTokenException $e) {
if (!$oParserState->comes(']')) {
throw $e;
}
}
} else {
$aNames[] = $oParserState->parseIdentifier();
}
$oParserState->consumeWhiteSpace();
} while (!$oParserState->comes(']'));
$oParserState->consume(']');
return new LineName($aNames, $oParserState->currentLine());
}
/**
* @return string
*/
public function __toString()
{
return $this->render(new OutputFormat());
}
public function __toString() {
return $this->render(new \Sabberworm\CSS\OutputFormat());
}
public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) {
return '[' . parent::render(\Sabberworm\CSS\OutputFormat::createCompact()) . ']';
}
/**
* @return string
*/
public function render(OutputFormat $oOutputFormat)
{
return '[' . parent::render(OutputFormat::createCompact()) . ']';
}
}

View File

@ -2,9 +2,13 @@
namespace Sabberworm\CSS\Value;
abstract class PrimitiveValue extends Value {
public function __construct($iLineNo = 0) {
abstract class PrimitiveValue extends Value
{
/**
* @param int $iLineNo
*/
public function __construct($iLineNo = 0)
{
parent::__construct($iLineNo);
}
}
}

View File

@ -2,8 +2,14 @@
namespace Sabberworm\CSS\Value;
class RuleValueList extends ValueList {
public function __construct($sSeparator = ',', $iLineNo = 0) {
parent::__construct(array(), $sSeparator, $iLineNo);
}
}
class RuleValueList extends ValueList
{
/**
* @param string $sSeparator
* @param int $iLineNo
*/
public function __construct($sSeparator = ',', $iLineNo = 0)
{
parent::__construct([], $sSeparator, $iLineNo);
}
}

View File

@ -2,121 +2,208 @@
namespace Sabberworm\CSS\Value;
use Sabberworm\CSS\OutputFormat;
use Sabberworm\CSS\Parsing\ParserState;
use Sabberworm\CSS\Parsing\UnexpectedEOFException;
use Sabberworm\CSS\Parsing\UnexpectedTokenException;
class Size extends PrimitiveValue {
class Size extends PrimitiveValue
{
/**
* vh/vw/vm(ax)/vmin/rem are absolute insofar as they dont scale to the immediate parent (only the viewport)
*
* @var array<int, string>
*/
const ABSOLUTE_SIZE_UNITS = ['px', 'cm', 'mm', 'mozmm', 'in', 'pt', 'pc', 'vh', 'vw', 'vmin', 'vmax', 'rem'];
const ABSOLUTE_SIZE_UNITS = 'px/cm/mm/mozmm/in/pt/pc/vh/vw/vm/vmin/vmax/rem'; //vh/vw/vm(ax)/vmin/rem are absolute insofar as they dont scale to the immediate parent (only the viewport)
const RELATIVE_SIZE_UNITS = '%/em/ex/ch/fr';
const NON_SIZE_UNITS = 'deg/grad/rad/s/ms/turns/Hz/kHz';
/**
* @var array<int, string>
*/
const RELATIVE_SIZE_UNITS = ['%', 'em', 'ex', 'ch', 'fr'];
private static $SIZE_UNITS = null;
/**
* @var array<int, string>
*/
const NON_SIZE_UNITS = ['deg', 'grad', 'rad', 's', 'ms', 'turns', 'Hz', 'kHz'];
private $fSize;
private $sUnit;
private $bIsColorComponent;
/**
* @var array<int, array<string, string>>|null
*/
private static $SIZE_UNITS = null;
public function __construct($fSize, $sUnit = null, $bIsColorComponent = false, $iLineNo = 0) {
parent::__construct($iLineNo);
$this->fSize = floatval($fSize);
$this->sUnit = $sUnit;
$this->bIsColorComponent = $bIsColorComponent;
}
/**
* @var float
*/
private $fSize;
public static function parse(ParserState $oParserState, $bIsColorComponent = false) {
$sSize = '';
if ($oParserState->comes('-')) {
$sSize .= $oParserState->consume('-');
}
while (is_numeric($oParserState->peek()) || $oParserState->comes('.')) {
if ($oParserState->comes('.')) {
$sSize .= $oParserState->consume('.');
} else {
$sSize .= $oParserState->consume(1);
}
}
/**
* @var string|null
*/
private $sUnit;
$sUnit = null;
$aSizeUnits = self::getSizeUnits();
foreach($aSizeUnits as $iLength => &$aValues) {
$sKey = strtolower($oParserState->peek($iLength));
if(array_key_exists($sKey, $aValues)) {
if (($sUnit = $aValues[$sKey]) !== null) {
$oParserState->consume($iLength);
break;
}
}
}
return new Size(floatval($sSize), $sUnit, $bIsColorComponent, $oParserState->currentLine());
}
/**
* @var bool
*/
private $bIsColorComponent;
private static function getSizeUnits() {
if(self::$SIZE_UNITS === null) {
self::$SIZE_UNITS = array();
foreach (explode('/', Size::ABSOLUTE_SIZE_UNITS.'/'.Size::RELATIVE_SIZE_UNITS.'/'.Size::NON_SIZE_UNITS) as $val) {
$iSize = strlen($val);
if(!isset(self::$SIZE_UNITS[$iSize])) {
self::$SIZE_UNITS[$iSize] = array();
}
self::$SIZE_UNITS[$iSize][strtolower($val)] = $val;
}
/**
* @param float|int|string $fSize
* @param string|null $sUnit
* @param bool $bIsColorComponent
* @param int $iLineNo
*/
public function __construct($fSize, $sUnit = null, $bIsColorComponent = false, $iLineNo = 0)
{
parent::__construct($iLineNo);
$this->fSize = (float)$fSize;
$this->sUnit = $sUnit;
$this->bIsColorComponent = $bIsColorComponent;
}
// FIXME: Should we not order the longest units first?
ksort(self::$SIZE_UNITS, SORT_NUMERIC);
}
/**
* @param bool $bIsColorComponent
*
* @return Size
*
* @throws UnexpectedEOFException
* @throws UnexpectedTokenException
*/
public static function parse(ParserState $oParserState, $bIsColorComponent = false)
{
$sSize = '';
if ($oParserState->comes('-')) {
$sSize .= $oParserState->consume('-');
}
while (is_numeric($oParserState->peek()) || $oParserState->comes('.')) {
if ($oParserState->comes('.')) {
$sSize .= $oParserState->consume('.');
} else {
$sSize .= $oParserState->consume(1);
}
}
return self::$SIZE_UNITS;
}
$sUnit = null;
$aSizeUnits = self::getSizeUnits();
foreach ($aSizeUnits as $iLength => &$aValues) {
$sKey = strtolower($oParserState->peek($iLength));
if (array_key_exists($sKey, $aValues)) {
if (($sUnit = $aValues[$sKey]) !== null) {
$oParserState->consume($iLength);
break;
}
}
}
return new Size((float)$sSize, $sUnit, $bIsColorComponent, $oParserState->currentLine());
}
public function setUnit($sUnit) {
$this->sUnit = $sUnit;
}
/**
* @return array<int, array<string, string>>
*/
private static function getSizeUnits()
{
if (!is_array(self::$SIZE_UNITS)) {
self::$SIZE_UNITS = [];
foreach (array_merge(self::ABSOLUTE_SIZE_UNITS, self::RELATIVE_SIZE_UNITS, self::NON_SIZE_UNITS) as $val) {
$iSize = strlen($val);
if (!isset(self::$SIZE_UNITS[$iSize])) {
self::$SIZE_UNITS[$iSize] = [];
}
self::$SIZE_UNITS[$iSize][strtolower($val)] = $val;
}
public function getUnit() {
return $this->sUnit;
}
krsort(self::$SIZE_UNITS, SORT_NUMERIC);
}
public function setSize($fSize) {
$this->fSize = floatval($fSize);
}
return self::$SIZE_UNITS;
}
public function getSize() {
return $this->fSize;
}
/**
* @param string $sUnit
*
* @return void
*/
public function setUnit($sUnit)
{
$this->sUnit = $sUnit;
}
public function isColorComponent() {
return $this->bIsColorComponent;
}
/**
* @return string|null
*/
public function getUnit()
{
return $this->sUnit;
}
/**
* Returns whether the number stored in this Size really represents a size (as in a length of something on screen).
* @return false if the unit an angle, a duration, a frequency or the number is a component in a Color object.
*/
public function isSize() {
if (in_array($this->sUnit, explode('/', self::NON_SIZE_UNITS))) {
return false;
}
return !$this->isColorComponent();
}
/**
* @param float|int|string $fSize
*/
public function setSize($fSize)
{
$this->fSize = (float)$fSize;
}
public function isRelative() {
if (in_array($this->sUnit, explode('/', self::RELATIVE_SIZE_UNITS))) {
return true;
}
if ($this->sUnit === null && $this->fSize != 0) {
return true;
}
return false;
}
/**
* @return float
*/
public function getSize()
{
return $this->fSize;
}
public function __toString() {
return $this->render(new \Sabberworm\CSS\OutputFormat());
}
/**
* @return bool
*/
public function isColorComponent()
{
return $this->bIsColorComponent;
}
public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) {
$l = localeconv();
$sPoint = preg_quote($l['decimal_point'], '/');
return preg_replace(array("/$sPoint/", "/^(-?)0\./"), array('.', '$1.'), $this->fSize) . ($this->sUnit === null ? '' : $this->sUnit);
}
/**
* Returns whether the number stored in this Size really represents a size (as in a length of something on screen).
*
* @return false if the unit an angle, a duration, a frequency or the number is a component in a Color object.
*/
public function isSize()
{
if (in_array($this->sUnit, self::NON_SIZE_UNITS, true)) {
return false;
}
return !$this->isColorComponent();
}
/**
* @return bool
*/
public function isRelative()
{
if (in_array($this->sUnit, self::RELATIVE_SIZE_UNITS, true)) {
return true;
}
if ($this->sUnit === null && $this->fSize != 0) {
return true;
}
return false;
}
/**
* @return string
*/
public function __toString()
{
return $this->render(new OutputFormat());
}
/**
* @return string
*/
public function render(OutputFormat $oOutputFormat)
{
$l = localeconv();
$sPoint = preg_quote($l['decimal_point'], '/');
$sSize = preg_match("/[\d\.]+e[+-]?\d+/i", (string)$this->fSize)
? preg_replace("/$sPoint?0+$/", "", sprintf("%f", $this->fSize)) : $this->fSize;
return preg_replace(["/$sPoint/", "/^(-?)0\./"], ['.', '$1.'], $sSize)
. ($this->sUnit === null ? '' : $this->sUnit);
}
}

View File

@ -2,48 +2,81 @@
namespace Sabberworm\CSS\Value;
use Sabberworm\CSS\OutputFormat;
use Sabberworm\CSS\Parsing\ParserState;
use Sabberworm\CSS\Parsing\SourceException;
use Sabberworm\CSS\Parsing\UnexpectedEOFException;
use Sabberworm\CSS\Parsing\UnexpectedTokenException;
class URL extends PrimitiveValue {
class URL extends PrimitiveValue
{
/**
* @var CSSString
*/
private $oURL;
private $oURL;
/**
* @param int $iLineNo
*/
public function __construct(CSSString $oURL, $iLineNo = 0)
{
parent::__construct($iLineNo);
$this->oURL = $oURL;
}
public function __construct(CSSString $oURL, $iLineNo = 0) {
parent::__construct($iLineNo);
$this->oURL = $oURL;
}
/**
* @return URL
*
* @throws SourceException
* @throws UnexpectedEOFException
* @throws UnexpectedTokenException
*/
public static function parse(ParserState $oParserState)
{
$bUseUrl = $oParserState->comes('url', true);
if ($bUseUrl) {
$oParserState->consume('url');
$oParserState->consumeWhiteSpace();
$oParserState->consume('(');
}
$oParserState->consumeWhiteSpace();
$oResult = new URL(CSSString::parse($oParserState), $oParserState->currentLine());
if ($bUseUrl) {
$oParserState->consumeWhiteSpace();
$oParserState->consume(')');
}
return $oResult;
}
public static function parse(ParserState $oParserState) {
$bUseUrl = $oParserState->comes('url', true);
if ($bUseUrl) {
$oParserState->consume('url');
$oParserState->consumeWhiteSpace();
$oParserState->consume('(');
}
$oParserState->consumeWhiteSpace();
$oResult = new URL(CSSString::parse($oParserState), $oParserState->currentLine());
if ($bUseUrl) {
$oParserState->consumeWhiteSpace();
$oParserState->consume(')');
}
return $oResult;
}
/**
* @return void
*/
public function setURL(CSSString $oURL)
{
$this->oURL = $oURL;
}
/**
* @return CSSString
*/
public function getURL()
{
return $this->oURL;
}
public function setURL(CSSString $oURL) {
$this->oURL = $oURL;
}
/**
* @return string
*/
public function __toString()
{
return $this->render(new OutputFormat());
}
public function getURL() {
return $this->oURL;
}
public function __toString() {
return $this->render(new \Sabberworm\CSS\OutputFormat());
}
public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) {
return "url({$this->oURL->render($oOutputFormat)})";
}
}
/**
* @return string
*/
public function render(OutputFormat $oOutputFormat)
{
return "url({$this->oURL->render($oOutputFormat)})";
}
}

View File

@ -3,129 +3,196 @@
namespace Sabberworm\CSS\Value;
use Sabberworm\CSS\Parsing\ParserState;
use Sabberworm\CSS\Parsing\SourceException;
use Sabberworm\CSS\Parsing\UnexpectedEOFException;
use Sabberworm\CSS\Parsing\UnexpectedTokenException;
use Sabberworm\CSS\Renderable;
abstract class Value implements Renderable {
protected $iLineNo;
abstract class Value implements Renderable
{
/**
* @var int
*/
protected $iLineNo;
public function __construct($iLineNo = 0) {
$this->iLineNo = $iLineNo;
}
/**
* @param int $iLineNo
*/
public function __construct($iLineNo = 0)
{
$this->iLineNo = $iLineNo;
}
public static function parseValue(ParserState $oParserState, $aListDelimiters = array()) {
$aStack = array();
$oParserState->consumeWhiteSpace();
//Build a list of delimiters and parsed values
while (!($oParserState->comes('}') || $oParserState->comes(';') || $oParserState->comes('!') || $oParserState->comes(')') || $oParserState->comes('\\'))) {
if (count($aStack) > 0) {
$bFoundDelimiter = false;
foreach ($aListDelimiters as $sDelimiter) {
if ($oParserState->comes($sDelimiter)) {
array_push($aStack, $oParserState->consume($sDelimiter));
$oParserState->consumeWhiteSpace();
$bFoundDelimiter = true;
break;
}
}
if (!$bFoundDelimiter) {
//Whitespace was the list delimiter
array_push($aStack, ' ');
}
}
array_push($aStack, self::parsePrimitiveValue($oParserState));
$oParserState->consumeWhiteSpace();
}
//Convert the list to list objects
foreach ($aListDelimiters as $sDelimiter) {
if (count($aStack) === 1) {
return $aStack[0];
}
$iStartPosition = null;
while (($iStartPosition = array_search($sDelimiter, $aStack, true)) !== false) {
$iLength = 2; //Number of elements to be joined
for ($i = $iStartPosition + 2; $i < count($aStack); $i+=2, ++$iLength) {
if ($sDelimiter !== $aStack[$i]) {
break;
}
}
$oList = new RuleValueList($sDelimiter, $oParserState->currentLine());
for ($i = $iStartPosition - 1; $i - $iStartPosition + 1 < $iLength * 2; $i+=2) {
$oList->addListComponent($aStack[$i]);
}
array_splice($aStack, $iStartPosition - 1, $iLength * 2 - 1, array($oList));
}
}
if (!isset($aStack[0])) {
throw new UnexpectedTokenException(" {$oParserState->peek()} ", $oParserState->peek(1, -1) . $oParserState->peek(2), 'literal', $oParserState->currentLine());
}
return $aStack[0];
}
/**
* @param array<array-key, string> $aListDelimiters
*
* @return RuleValueList|CSSFunction|CSSString|LineName|Size|URL|string
*
* @throws UnexpectedTokenException
* @throws UnexpectedEOFException
*/
public static function parseValue(ParserState $oParserState, array $aListDelimiters = [])
{
/** @var array<int, RuleValueList|CSSFunction|CSSString|LineName|Size|URL|string> $aStack */
$aStack = [];
$oParserState->consumeWhiteSpace();
//Build a list of delimiters and parsed values
while (
!($oParserState->comes('}') || $oParserState->comes(';') || $oParserState->comes('!')
|| $oParserState->comes(')')
|| $oParserState->comes('\\'))
) {
if (count($aStack) > 0) {
$bFoundDelimiter = false;
foreach ($aListDelimiters as $sDelimiter) {
if ($oParserState->comes($sDelimiter)) {
array_push($aStack, $oParserState->consume($sDelimiter));
$oParserState->consumeWhiteSpace();
$bFoundDelimiter = true;
break;
}
}
if (!$bFoundDelimiter) {
//Whitespace was the list delimiter
array_push($aStack, ' ');
}
}
array_push($aStack, self::parsePrimitiveValue($oParserState));
$oParserState->consumeWhiteSpace();
}
// Convert the list to list objects
foreach ($aListDelimiters as $sDelimiter) {
if (count($aStack) === 1) {
return $aStack[0];
}
$iStartPosition = null;
while (($iStartPosition = array_search($sDelimiter, $aStack, true)) !== false) {
$iLength = 2; //Number of elements to be joined
for ($i = $iStartPosition + 2; $i < count($aStack); $i += 2, ++$iLength) {
if ($sDelimiter !== $aStack[$i]) {
break;
}
}
$oList = new RuleValueList($sDelimiter, $oParserState->currentLine());
for ($i = $iStartPosition - 1; $i - $iStartPosition + 1 < $iLength * 2; $i += 2) {
$oList->addListComponent($aStack[$i]);
}
array_splice($aStack, $iStartPosition - 1, $iLength * 2 - 1, [$oList]);
}
}
if (!isset($aStack[0])) {
throw new UnexpectedTokenException(
" {$oParserState->peek()} ",
$oParserState->peek(1, -1) . $oParserState->peek(2),
'literal',
$oParserState->currentLine()
);
}
return $aStack[0];
}
public static function parseIdentifierOrFunction(ParserState $oParserState, $bIgnoreCase = false) {
$sResult = $oParserState->parseIdentifier($bIgnoreCase);
/**
* @param bool $bIgnoreCase
*
* @return CSSFunction|string
*
* @throws UnexpectedEOFException
* @throws UnexpectedTokenException
*/
public static function parseIdentifierOrFunction(ParserState $oParserState, $bIgnoreCase = false)
{
$sResult = $oParserState->parseIdentifier($bIgnoreCase);
if ($oParserState->comes('(')) {
$oParserState->consume('(');
$aArguments = Value::parseValue($oParserState, array('=', ' ', ','));
$sResult = new CSSFunction($sResult, $aArguments, ',', $oParserState->currentLine());
$oParserState->consume(')');
}
if ($oParserState->comes('(')) {
$oParserState->consume('(');
$aArguments = Value::parseValue($oParserState, ['=', ' ', ',']);
$sResult = new CSSFunction($sResult, $aArguments, ',', $oParserState->currentLine());
$oParserState->consume(')');
}
return $sResult;
}
return $sResult;
}
public static function parsePrimitiveValue(ParserState $oParserState) {
$oValue = null;
$oParserState->consumeWhiteSpace();
if (is_numeric($oParserState->peek()) || ($oParserState->comes('-.') && is_numeric($oParserState->peek(1, 2))) || (($oParserState->comes('-') || $oParserState->comes('.')) && is_numeric($oParserState->peek(1, 1)))) {
$oValue = Size::parse($oParserState);
} else if ($oParserState->comes('#') || $oParserState->comes('rgb', true) || $oParserState->comes('hsl', true)) {
$oValue = Color::parse($oParserState);
} else if ($oParserState->comes('url', true)) {
$oValue = URL::parse($oParserState);
} else if ($oParserState->comes('calc', true) || $oParserState->comes('-webkit-calc', true) || $oParserState->comes('-moz-calc', true)) {
$oValue = CalcFunction::parse($oParserState);
} else if ($oParserState->comes("'") || $oParserState->comes('"')) {
$oValue = CSSString::parse($oParserState);
} else if ($oParserState->comes("progid:") && $oParserState->getSettings()->bLenientParsing) {
$oValue = self::parseMicrosoftFilter($oParserState);
} else if ($oParserState->comes("[")) {
$oValue = LineName::parse($oParserState);
} else if ($oParserState->comes("U+")) {
$oValue = self::parseUnicodeRangeValue($oParserState);
} else {
$oValue = self::parseIdentifierOrFunction($oParserState);
}
$oParserState->consumeWhiteSpace();
return $oValue;
}
/**
* @return CSSFunction|CSSString|LineName|Size|URL|string
*
* @throws UnexpectedEOFException
* @throws UnexpectedTokenException
* @throws SourceException
*/
public static function parsePrimitiveValue(ParserState $oParserState)
{
$oValue = null;
$oParserState->consumeWhiteSpace();
if (
is_numeric($oParserState->peek())
|| ($oParserState->comes('-.')
&& is_numeric($oParserState->peek(1, 2)))
|| (($oParserState->comes('-') || $oParserState->comes('.')) && is_numeric($oParserState->peek(1, 1)))
) {
$oValue = Size::parse($oParserState);
} elseif ($oParserState->comes('#') || $oParserState->comes('rgb', true) || $oParserState->comes('hsl', true)) {
$oValue = Color::parse($oParserState);
} elseif ($oParserState->comes('url', true)) {
$oValue = URL::parse($oParserState);
} elseif (
$oParserState->comes('calc', true) || $oParserState->comes('-webkit-calc', true)
|| $oParserState->comes('-moz-calc', true)
) {
$oValue = CalcFunction::parse($oParserState);
} elseif ($oParserState->comes("'") || $oParserState->comes('"')) {
$oValue = CSSString::parse($oParserState);
} elseif ($oParserState->comes("progid:") && $oParserState->getSettings()->bLenientParsing) {
$oValue = self::parseMicrosoftFilter($oParserState);
} elseif ($oParserState->comes("[")) {
$oValue = LineName::parse($oParserState);
} elseif ($oParserState->comes("U+")) {
$oValue = self::parseUnicodeRangeValue($oParserState);
} else {
$oValue = self::parseIdentifierOrFunction($oParserState);
}
$oParserState->consumeWhiteSpace();
return $oValue;
}
private static function parseMicrosoftFilter(ParserState $oParserState) {
$sFunction = $oParserState->consumeUntil('(', false, true);
$aArguments = Value::parseValue($oParserState, array(',', '='));
return new CSSFunction($sFunction, $aArguments, ',', $oParserState->currentLine());
}
/**
* @return CSSFunction
*
* @throws UnexpectedEOFException
* @throws UnexpectedTokenException
*/
private static function parseMicrosoftFilter(ParserState $oParserState)
{
$sFunction = $oParserState->consumeUntil('(', false, true);
$aArguments = Value::parseValue($oParserState, [',', '=']);
return new CSSFunction($sFunction, $aArguments, ',', $oParserState->currentLine());
}
private static function parseUnicodeRangeValue(ParserState $oParserState) {
$iCodepointMaxLenth = 6; // Code points outside BMP can use up to six digits
$sRange = "";
$oParserState->consume("U+");
do {
if ($oParserState->comes('-')) $iCodepointMaxLenth = 13; // Max length is 2 six digit code points + the dash(-) between them
$sRange .= $oParserState->consume(1);
} while (strlen($sRange) < $iCodepointMaxLenth && preg_match("/[A-Fa-f0-9\?-]/", $oParserState->peek()));
return "U+{$sRange}";
}
/**
* @return int
*/
public function getLineNo() {
return $this->iLineNo;
}
/**
* @return string
*
* @throws UnexpectedEOFException
* @throws UnexpectedTokenException
*/
private static function parseUnicodeRangeValue(ParserState $oParserState)
{
$iCodepointMaxLength = 6; // Code points outside BMP can use up to six digits
$sRange = "";
$oParserState->consume("U+");
do {
if ($oParserState->comes('-')) {
$iCodepointMaxLength = 13; // Max length is 2 six digit code points + the dash(-) between them
}
$sRange .= $oParserState->consume(1);
} while (strlen($sRange) < $iCodepointMaxLength && preg_match("/[A-Fa-f0-9\?-]/", $oParserState->peek()));
return "U+{$sRange}";
}
//Methods are commented out because re-declaring them here is a fatal error in PHP < 5.3.9
//public abstract function __toString();
//public abstract function render(\Sabberworm\CSS\OutputFormat $oOutputFormat);
/**
* @return int
*/
public function getLineNo()
{
return $this->iLineNo;
}
}

View File

@ -2,46 +2,99 @@
namespace Sabberworm\CSS\Value;
abstract class ValueList extends Value {
use Sabberworm\CSS\OutputFormat;
protected $aComponents;
protected $sSeparator;
abstract class ValueList extends Value
{
/**
* @var array<int, RuleValueList|CSSFunction|CSSString|LineName|Size|URL|string>
*/
protected $aComponents;
public function __construct($aComponents = array(), $sSeparator = ',', $iLineNo = 0) {
parent::__construct($iLineNo);
if (!is_array($aComponents)) {
$aComponents = array($aComponents);
}
$this->aComponents = $aComponents;
$this->sSeparator = $sSeparator;
}
/**
* @var string
*/
protected $sSeparator;
public function addListComponent($mComponent) {
$this->aComponents[] = $mComponent;
}
/**
* phpcs:ignore Generic.Files.LineLength
* @param array<int, RuleValueList|CSSFunction|CSSString|LineName|Size|URL|string>|RuleValueList|CSSFunction|CSSString|LineName|Size|URL|string $aComponents
* @param string $sSeparator
* @param int $iLineNo
*/
public function __construct($aComponents = [], $sSeparator = ',', $iLineNo = 0)
{
parent::__construct($iLineNo);
if (!is_array($aComponents)) {
$aComponents = [$aComponents];
}
$this->aComponents = $aComponents;
$this->sSeparator = $sSeparator;
}
public function getListComponents() {
return $this->aComponents;
}
/**
* @param RuleValueList|CSSFunction|CSSString|LineName|Size|URL|string $mComponent
*
* @return void
*/
public function addListComponent($mComponent)
{
$this->aComponents[] = $mComponent;
}
public function setListComponents($aComponents) {
$this->aComponents = $aComponents;
}
/**
* @return array<int, RuleValueList|CSSFunction|CSSString|LineName|Size|URL|string>
*/
public function getListComponents()
{
return $this->aComponents;
}
public function getListSeparator() {
return $this->sSeparator;
}
/**
* @param array<int, RuleValueList|CSSFunction|CSSString|LineName|Size|URL|string> $aComponents
*
* @return void
*/
public function setListComponents(array $aComponents)
{
$this->aComponents = $aComponents;
}
public function setListSeparator($sSeparator) {
$this->sSeparator = $sSeparator;
}
/**
* @return string
*/
public function getListSeparator()
{
return $this->sSeparator;
}
public function __toString() {
return $this->render(new \Sabberworm\CSS\OutputFormat());
}
/**
* @param string $sSeparator
*
* @return void
*/
public function setListSeparator($sSeparator)
{
$this->sSeparator = $sSeparator;
}
public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) {
return $oOutputFormat->implode($oOutputFormat->spaceBeforeListArgumentSeparator($this->sSeparator) . $this->sSeparator . $oOutputFormat->spaceAfterListArgumentSeparator($this->sSeparator), $this->aComponents);
}
/**
* @return string
*/
public function __toString()
{
return $this->render(new OutputFormat());
}
/**
* @return string
*/
public function render(OutputFormat $oOutputFormat)
{
return $oOutputFormat->implode(
$oOutputFormat->spaceBeforeListArgumentSeparator($this->sSeparator) . $this->sSeparator
. $oOutputFormat->spaceAfterListArgumentSeparator($this->sSeparator),
$this->aComponents
);
}
}

View File

@ -343,7 +343,7 @@ All rights reserved.</copyright>
<location>php-css-parser</location>
<name>PHP-CSS-Parser</name>
<description>A Parser for CSS Files written in PHP.</description>
<version>8.3.1</version>
<version>8.4.0</version>
<license>MIT</license>
<repository>https://github.com/sabberworm/PHP-CSS-Parser</repository>
<copyrights>