plugin updates

This commit is contained in:
Tony Volpe
2024-09-17 10:43:54 -04:00
parent 44b413346f
commit b7c8882c8c
1359 changed files with 58219 additions and 11364 deletions

View File

@@ -61,13 +61,14 @@ class AtRuleBlockList extends CSSBlockList implements AtRule
*/
public function render(OutputFormat $oOutputFormat)
{
$sResult = $oOutputFormat->comments($this);
$sResult .= $oOutputFormat->sBeforeAtRuleBlock;
$sArgs = $this->sArgs;
if ($sArgs) {
$sArgs = ' ' . $sArgs;
}
$sResult = $oOutputFormat->sBeforeAtRuleBlock;
$sResult .= "@{$this->sType}$sArgs{$oOutputFormat->spaceBeforeOpeningBrace()}{";
$sResult .= parent::render($oOutputFormat);
$sResult .= $this->renderListContents($oOutputFormat);
$sResult .= '}';
$sResult .= $oOutputFormat->sAfterAtRuleBlock;
return $sResult;

View File

@@ -24,10 +24,10 @@ 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.
* This is the most generic container available. It can contain `DeclarationBlock`s (rule sets with a selector),
* `RuleSet`s as well as other `CSSList` objects.
*
* Also, it may contain `Import` and `Charset` objects stemming from at-rules.
* It can also contain `Import` and `Charset` objects stemming from at-rules.
*/
abstract class CSSList implements Renderable, Commentable
{
@@ -69,8 +69,9 @@ abstract class CSSList implements Renderable, Commentable
$oParserState = new ParserState($oParserState, Settings::create());
}
$bLenientParsing = $oParserState->getSettings()->bLenientParsing;
$aComments = [];
while (!$oParserState->isEnd()) {
$comments = $oParserState->consumeWhiteSpace();
$aComments = array_merge($aComments, $oParserState->consumeWhiteSpace());
$oListItem = null;
if ($bLenientParsing) {
try {
@@ -86,11 +87,12 @@ abstract class CSSList implements Renderable, Commentable
return;
}
if ($oListItem) {
$oListItem->setComments($comments);
$oListItem->addComments($aComments);
$oList->append($oListItem);
}
$oParserState->consumeWhiteSpace();
$aComments = $oParserState->consumeWhiteSpace();
}
$oList->addComments($aComments);
if (!$bIsRoot && !$bLenientParsing) {
throw new SourceException("Unexpected end of document", $oParserState->currentLine());
}
@@ -125,22 +127,19 @@ abstract class CSSList implements Renderable, Commentable
$oParserState->currentLine()
);
}
$oParserState->setCharset($oAtRule->getCharset()->getString());
$oParserState->setCharset($oAtRule->getCharset());
}
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());
}
if ($bIsRoot) {
if ($oParserState->getSettings()->bLenientParsing) {
return DeclarationBlock::parse($oParserState);
} else {
return null;
throw new SourceException("Unopened {", $oParserState->currentLine());
}
} else {
// End of list
return null;
}
} else {
return DeclarationBlock::parse($oParserState, $oList);
@@ -172,10 +171,10 @@ abstract class CSSList implements Renderable, Commentable
$oParserState->consumeUntil([';', ParserState::EOF], true, true);
return new Import($oLocation, $sMediaQuery ?: null, $iIdentifierLineNum);
} elseif ($sIdentifier === 'charset') {
$sCharset = CSSString::parse($oParserState);
$oCharsetString = CSSString::parse($oParserState);
$oParserState->consumeWhiteSpace();
$oParserState->consumeUntil([';', ParserState::EOF], true, true);
return new Charset($sCharset, $iIdentifierLineNum);
return new Charset($oCharsetString, $iIdentifierLineNum);
} elseif (self::identifierIs($sIdentifier, 'keyframes')) {
$oResult = new KeyFrame($iIdentifierLineNum);
$oResult->setVendorKeyFrame($sIdentifier);
@@ -272,7 +271,7 @@ abstract class CSSList implements Renderable, Commentable
}
/**
* Appends an item to tje list of contents.
* Appends an item to the list of contents.
*
* @param RuleSet|CSSList|Import|Charset $oItem
*
@@ -297,6 +296,22 @@ abstract class CSSList implements Renderable, Commentable
array_splice($this->aContents, $iOffset, $iLength, $mReplacement);
}
/**
* Inserts an item in the CSS list before its sibling. If the desired sibling cannot be found,
* the item is appended at the end.
*
* @param RuleSet|CSSList|Import|Charset $item
* @param RuleSet|CSSList|Import|Charset $sibling
*/
public function insertBefore($item, $sibling)
{
if (in_array($sibling, $this->aContents, true)) {
$this->replace($sibling, [$item, $sibling]);
} else {
$this->append($item);
}
}
/**
* Removes an item from the CSS list.
*
@@ -402,7 +417,7 @@ abstract class CSSList implements Renderable, Commentable
/**
* @return string
*/
public function render(OutputFormat $oOutputFormat)
protected function renderListContents(OutputFormat $oOutputFormat)
{
$sResult = '';
$bIsFirst = true;
@@ -442,6 +457,8 @@ abstract class CSSList implements Renderable, Commentable
abstract public function isRootList();
/**
* Returns the stored items.
*
* @return array<int, RuleSet|Import|Charset|CSSList>
*/
public function getContents()

View File

@@ -11,8 +11,8 @@ 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 at-rules encountered.
* This class represents the root of a parsed CSS file. It contains all top-level CSS contents: mostly declaration
* blocks, but also any at-rules encountered (`Import` and `Charset`).
*/
class Document extends CSSBlockList
{
@@ -37,7 +37,8 @@ class Document extends CSSBlockList
}
/**
* Gets all `DeclarationBlock` objects recursively.
* Gets all `DeclarationBlock` objects recursively, no matter how deeply nested the selectors are.
* Aliased as `getAllSelectors()`.
*
* @return array<int, DeclarationBlock>
*/
@@ -62,7 +63,7 @@ class Document extends CSSBlockList
}
/**
* Returns all `RuleSet` objects found recursively in the tree.
* Returns all `RuleSet` objects recursively found in the tree, no matter how deeply nested the rule sets are.
*
* @return array<int, RuleSet>
*/
@@ -75,7 +76,7 @@ class Document extends CSSBlockList
}
/**
* Returns all `Value` objects found recursively in the tree.
* Returns all `Value` objects found recursively in `Rule`s in the tree.
*
* @param CSSList|RuleSet|string $mElement
* the `CSSList` or `RuleSet` to start the search from (defaults to the whole document).
@@ -102,7 +103,7 @@ class Document extends CSSBlockList
}
/**
* Returns all `Selector` objects found recursively in the tree.
* Returns all `Selector` objects with the requested specificity 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).
@@ -159,7 +160,7 @@ class Document extends CSSBlockList
if ($oOutputFormat === null) {
$oOutputFormat = new OutputFormat();
}
return parent::render($oOutputFormat);
return $oOutputFormat->comments($this) . $this->renderListContents($oOutputFormat);
}
/**

View File

@@ -72,8 +72,9 @@ class KeyFrame extends CSSList implements AtRule
*/
public function render(OutputFormat $oOutputFormat)
{
$sResult = "@{$this->vendorKeyFrame} {$this->animationName}{$oOutputFormat->spaceBeforeOpeningBrace()}{";
$sResult .= parent::render($oOutputFormat);
$sResult = $oOutputFormat->comments($this);
$sResult .= "@{$this->vendorKeyFrame} {$this->animationName}{$oOutputFormat->spaceBeforeOpeningBrace()}{";
$sResult .= $this->renderListContents($oOutputFormat);
$sResult .= '}';
return $sResult;
}

View File

@@ -143,6 +143,13 @@ class OutputFormat
*/
public $bIgnoreExceptions = false;
/**
* Render comments for lists and RuleSets
*
* @var bool
*/
public $bRenderComments = false;
/**
* @var OutputFormatter|null
*/
@@ -314,8 +321,12 @@ class OutputFormat
public static function createCompact()
{
$format = self::create();
$format->set('Space*Rules', "")->set('Space*Blocks', "")->setSpaceAfterRuleName('')
->setSpaceBeforeOpeningBrace('')->setSpaceAfterSelectorSeparator('');
$format->set('Space*Rules', "")
->set('Space*Blocks', "")
->setSpaceAfterRuleName('')
->setSpaceBeforeOpeningBrace('')
->setSpaceAfterSelectorSeparator('')
->setRenderComments(false);
return $format;
}
@@ -327,8 +338,11 @@ class OutputFormat
public static function createPretty()
{
$format = self::create();
$format->set('Space*Rules', "\n")->set('Space*Blocks', "\n")
->setSpaceBetweenBlocks("\n\n")->set('SpaceAfterListArgumentSeparator', ['default' => '', ',' => ' ']);
$format->set('Space*Rules', "\n")
->set('Space*Blocks', "\n")
->setSpaceBetweenBlocks("\n\n")
->set('SpaceAfterListArgumentSeparator', ['default' => '', ',' => ' '])
->setRenderComments(true);
return $format;
}
}

View File

@@ -2,6 +2,7 @@
namespace Sabberworm\CSS;
use Sabberworm\CSS\Comment\Commentable;
use Sabberworm\CSS\Parsing\OutputException;
class OutputFormatter
@@ -211,6 +212,29 @@ class OutputFormatter
return implode(';', $sString);
}
/**
*
* @param array<Commentable> $aComments
*
* @return string
*/
public function comments(Commentable $oCommentable)
{
if (!$this->oFormat->bRenderComments) {
return '';
}
$sResult = '';
$aComments = $oCommentable->getComments();
$iLastCommentIndex = count($aComments) - 1;
foreach ($aComments as $i => $oComment) {
$sResult .= $oComment->render($this->oFormat);
$sResult .= $i === $iLastCommentIndex ? $this->spaceAfterBlocks() : $this->spaceBetweenBlocks();
}
return $sResult;
}
/**
* @param string $sSpaceString
*

View File

@@ -17,7 +17,7 @@ class Parser
private $oParserState;
/**
* @param string $sText
* @param string $sText the complete CSS as text (i.e., usually the contents of a CSS file)
* @param Settings|null $oParserSettings
* @param int $iLineNo the line number (starting from 1, not from 0)
*/
@@ -30,6 +30,8 @@ class Parser
}
/**
* Sets the charset to be used if the CSS does not contain an `@charset` declaration.
*
* @param string $sCharset
*
* @return void
@@ -40,6 +42,8 @@ class Parser
}
/**
* Returns the charset that is used if the CSS does not contain an `@charset` declaration.
*
* @return void
*/
public function getCharset()
@@ -49,6 +53,8 @@ class Parser
}
/**
* Parses the CSS provided to the constructor and creates a `Document` from it.
*
* @return Document
*
* @throws SourceException

View File

@@ -0,0 +1,34 @@
<?php
namespace Sabberworm\CSS\Parsing;
class Anchor
{
/**
* @var int
*/
private $iPosition;
/**
* @var \Sabberworm\CSS\Parsing\ParserState
*/
private $oParserState;
/**
* @param int $iPosition
* @param \Sabberworm\CSS\Parsing\ParserState $oParserState
*/
public function __construct($iPosition, ParserState $oParserState)
{
$this->iPosition = $iPosition;
$this->oParserState = $oParserState;
}
/**
* @return void
*/
public function backtrack()
{
$this->oParserState->setPosition($this->iPosition);
}
}

View File

@@ -9,6 +9,8 @@ class ParserState
{
/**
* @var null
*
* @internal
*/
const EOF = null;
@@ -33,6 +35,8 @@ class ParserState
private $iCurrentPosition;
/**
* will only be used if the CSS does not contain an `@charset` declaration
*
* @var string
*/
private $sCharset;
@@ -48,7 +52,7 @@ class ParserState
private $iLineNo;
/**
* @param string $sText
* @param string $sText the complete CSS as text (i.e., usually the contents of a CSS file)
* @param int $iLineNo
*/
public function __construct($sText, Settings $oParserSettings, $iLineNo = 1)
@@ -61,6 +65,8 @@ class ParserState
}
/**
* Sets the charset to be used if the CSS does not contain an `@charset` declaration.
*
* @param string $sCharset
*
* @return void
@@ -75,6 +81,8 @@ class ParserState
}
/**
* Returns the charset that is used if the CSS does not contain an `@charset` declaration.
*
* @return string
*/
public function getCharset()
@@ -106,6 +114,24 @@ class ParserState
return $this->oParserSettings;
}
/**
* @return \Sabberworm\CSS\Parsing\Anchor
*/
public function anchor()
{
return new Anchor($this->iCurrentPosition, $this);
}
/**
* @param int $iPosition
*
* @return void
*/
public function setPosition($iPosition)
{
$this->iCurrentPosition = $iPosition;
}
/**
* @param bool $bIgnoreCase
*
@@ -115,12 +141,15 @@ class ParserState
*/
public function parseIdentifier($bIgnoreCase = true)
{
if ($this->isEnd()) {
throw new UnexpectedEOFException('', '', 'identifier', $this->iLineNo);
}
$sResult = $this->parseCharacter(true);
if ($sResult === null) {
throw new UnexpectedTokenException($sResult, $this->peek(5), 'identifier', $this->iLineNo);
}
$sCharacter = null;
while (($sCharacter = $this->parseCharacter(true)) !== null) {
while (!$this->isEnd() && ($sCharacter = $this->parseCharacter(true)) !== null) {
if (preg_match('/[a-zA-Z0-9\x{00A0}-\x{FFFF}_-]/Sux', $sCharacter)) {
$sResult .= $sCharacter;
} else {
@@ -204,7 +233,7 @@ class ParserState
*/
public function consumeWhiteSpace()
{
$comments = [];
$aComments = [];
do {
while (preg_match('/\\s/isSu', $this->peek()) === 1) {
$this->consume(1);
@@ -214,16 +243,16 @@ class ParserState
$oComment = $this->consumeComment();
} catch (UnexpectedEOFException $e) {
$this->iCurrentPosition = $this->iLength;
return;
return $aComments;
}
} else {
$oComment = $this->consumeComment();
}
if ($oComment !== false) {
$comments[] = $oComment;
$aComments[] = $oComment;
}
} while ($oComment !== false);
return $comments;
return $aComments;
}
/**

View File

@@ -12,6 +12,8 @@ interface AtRule extends Renderable, Commentable
* were whitelisting the block rules and have anything else be treated as a set rule.
*
* @var string
*
* @internal
*/
const BLOCK_RULES = 'media/document/supports/region-style/font-feature-values';
@@ -19,6 +21,8 @@ interface AtRule extends Renderable, Commentable
* … and more font-specific ones (to be used inside font-feature-values)
*
* @var string
*
* @internal
*/
const SET_RULES = 'font-face/counter-style/page/swash/styleset/annotation';

View File

@@ -4,6 +4,7 @@ namespace Sabberworm\CSS\Property;
use Sabberworm\CSS\Comment\Comment;
use Sabberworm\CSS\OutputFormat;
use Sabberworm\CSS\Value\CSSString;
/**
* Class representing an `@charset` rule.
@@ -16,9 +17,9 @@ use Sabberworm\CSS\OutputFormat;
class Charset implements AtRule
{
/**
* @var string
* @var CSSString
*/
private $sCharset;
private $oCharset;
/**
* @var int
@@ -31,12 +32,12 @@ class Charset implements AtRule
protected $aComments;
/**
* @param string $sCharset
* @param CSSString $oCharset
* @param int $iLineNo
*/
public function __construct($sCharset, $iLineNo = 0)
public function __construct(CSSString $oCharset, $iLineNo = 0)
{
$this->sCharset = $sCharset;
$this->oCharset = $oCharset;
$this->iLineNo = $iLineNo;
$this->aComments = [];
}
@@ -50,13 +51,14 @@ class Charset implements AtRule
}
/**
* @param string $sCharset
* @param string|CSSString $oCharset
*
* @return void
*/
public function setCharset($sCharset)
{
$this->sCharset = $sCharset;
$sCharset = $sCharset instanceof CSSString ? $sCharset : new CSSString($sCharset);
$this->oCharset = $sCharset;
}
/**
@@ -64,7 +66,7 @@ class Charset implements AtRule
*/
public function getCharset()
{
return $this->sCharset;
return $this->oCharset->getString();
}
/**
@@ -80,7 +82,7 @@ class Charset implements AtRule
*/
public function render(OutputFormat $oOutputFormat)
{
return "@charset {$this->sCharset->render($oOutputFormat)};";
return "{$oOutputFormat->comments($this)}@charset {$this->oCharset->render($oOutputFormat)};";
}
/**
@@ -96,7 +98,7 @@ class Charset implements AtRule
*/
public function atRuleArgs()
{
return $this->sCharset;
return $this->oCharset;
}
/**

View File

@@ -83,7 +83,7 @@ class Import implements AtRule
*/
public function render(OutputFormat $oOutputFormat)
{
return "@import " . $this->oLocation->render($oOutputFormat)
return $oOutputFormat->comments($this) . "@import " . $this->oLocation->render($oOutputFormat)
. ($this->sMediaQuery === null ? '' : ' ' . $this->sMediaQuery) . ';';
}
@@ -134,4 +134,12 @@ class Import implements AtRule
{
$this->aComments = $aComments;
}
/**
* @return string
*/
public function getMediaQuery()
{
return $this->sMediaQuery;
}
}

View File

@@ -8,6 +8,8 @@ class KeyframeSelector extends Selector
* regexp for specificity calculations
*
* @var string
*
* @internal
*/
const SELECTOR_VALIDATION_RX = '/
^(

View File

@@ -12,6 +12,8 @@ class Selector
* regexp for specificity calculations
*
* @var string
*
* @internal
*/
const NON_ID_ATTRIBUTES_AND_PSEUDO_CLASSES_RX = '/
(\.[\w]+) # classes
@@ -36,6 +38,8 @@ class Selector
* regexp for specificity calculations
*
* @var string
*
* @internal
*/
const ELEMENTS_AND_PSEUDO_ELEMENTS_RX = '/
((^|[\s\+\>\~]+)[\w]+ # elements
@@ -49,6 +53,8 @@ class Selector
* regexp for specificity calculations
*
* @var string
*
* @internal
*/
const SELECTOR_VALIDATION_RX = '/
^(

View File

@@ -13,8 +13,9 @@ use Sabberworm\CSS\Value\RuleValueList;
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];”
* `Rule`s just have a string key (the rule) and a 'Value'.
*
* In CSS, `Rule`s are expressed as follows: “key: value[0][0] value[0][1], value[1][0] value[1][1];”
*/
class Rule implements Renderable, Commentable
{
@@ -24,7 +25,7 @@ class Rule implements Renderable, Commentable
private $sRule;
/**
* @var RuleValueList|null
* @var RuleValueList|string|null
*/
private $mValue;
@@ -171,7 +172,7 @@ class Rule implements Renderable, Commentable
}
/**
* @return RuleValueList|null
* @return RuleValueList|string|null
*/
public function getValue()
{
@@ -179,7 +180,7 @@ class Rule implements Renderable, Commentable
}
/**
* @param RuleValueList|null $mValue
* @param RuleValueList|string|null $mValue
*
* @return void
*/
@@ -346,8 +347,8 @@ class Rule implements Renderable, Commentable
*/
public function render(OutputFormat $oOutputFormat)
{
$sResult = "{$this->sRule}:{$oOutputFormat->spaceAfterRuleName()}";
if ($this->mValue instanceof Value) { //Can also be a ValueList
$sResult = "{$oOutputFormat->comments($this)}{$this->sRule}:{$oOutputFormat->spaceAfterRuleName()}";
if ($this->mValue instanceof Value) { // Can also be a ValueList
$sResult .= $this->mValue->render($oOutputFormat);
} else {
$sResult .= $this->mValue;

View File

@@ -6,7 +6,10 @@ use Sabberworm\CSS\OutputFormat;
use Sabberworm\CSS\Property\AtRule;
/**
* A RuleSet constructed by an unknown at-rule. `@font-face` rules are rendered into AtRuleSet objects.
* This class represents rule sets for generic at-rules which are not covered by specific classes, i.e., not
* `@import`, `@charset` or `@media`.
*
* A common example for this is `@font-face`.
*/
class AtRuleSet extends RuleSet implements AtRule
{
@@ -61,12 +64,13 @@ class AtRuleSet extends RuleSet implements AtRule
*/
public function render(OutputFormat $oOutputFormat)
{
$sResult = $oOutputFormat->comments($this);
$sArgs = $this->sArgs;
if ($sArgs) {
$sArgs = ' ' . $sArgs;
}
$sResult = "@{$this->sType}$sArgs{$oOutputFormat->spaceBeforeOpeningBrace()}{";
$sResult .= parent::render($oOutputFormat);
$sResult .= "@{$this->sType}$sArgs{$oOutputFormat->spaceBeforeOpeningBrace()}{";
$sResult .= $this->renderRules($oOutputFormat);
$sResult .= '}';
return $sResult;
}

View File

@@ -19,7 +19,10 @@ use Sabberworm\CSS\Value\URL;
use Sabberworm\CSS\Value\Value;
/**
* Declaration blocks are the parts of a CSS file which denote the rules belonging to a selector.
* This class represents a `RuleSet` constrained by a `Selector`.
*
* It contains an array of selector objects (comma-separated in the CSS) as well as the rules to be applied to the
* matching elements.
*
* Declaration blocks usually appear directly inside a `Document` or another `CSSList` (mostly a `MediaQuery`).
*/
@@ -562,6 +565,7 @@ class DeclarationBlock extends RuleSet
public function createShorthandProperties(array $aProperties, $sShorthand)
{
$aRules = $this->getRulesAssoc();
$oRule = null;
$aNewValues = [];
foreach ($aProperties as $sProperty) {
if (!isset($aRules[$sProperty])) {
@@ -582,7 +586,7 @@ class DeclarationBlock extends RuleSet
$this->removeRule($sProperty);
}
}
if (count($aNewValues)) {
if ($aNewValues !== [] && $oRule instanceof Rule) {
$oNewRule = new Rule($sShorthand, $oRule->getLineNo(), $oRule->getColNo());
foreach ($aNewValues as $mValue) {
$oNewRule->addValue($mValue);
@@ -812,18 +816,19 @@ class DeclarationBlock extends RuleSet
*/
public function render(OutputFormat $oOutputFormat)
{
$sResult = $oOutputFormat->comments($this);
if (count($this->aSelectors) === 0) {
// If all the selectors have been removed, this declaration block becomes invalid
throw new OutputException("Attempt to print declaration block with missing selector", $this->iLineNo);
}
$sResult = $oOutputFormat->sBeforeDeclarationBlock;
$sResult .= $oOutputFormat->sBeforeDeclarationBlock;
$sResult .= $oOutputFormat->implode(
$oOutputFormat->spaceBeforeSelectorSeparator() . ',' . $oOutputFormat->spaceAfterSelectorSeparator(),
$this->aSelectors
);
$sResult .= $oOutputFormat->sAfterDeclarationBlockSelectors;
$sResult .= $oOutputFormat->spaceBeforeOpeningBrace() . '{';
$sResult .= parent::render($oOutputFormat);
$sResult .= $this->renderRules($oOutputFormat);
$sResult .= '}';
$sResult .= $oOutputFormat->sAfterDeclarationBlock;
return $sResult;

View File

@@ -12,8 +12,13 @@ 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.
* This class is a container for individual 'Rule's.
*
* The most common form of a rule set is one constrained by a selector, i.e., a `DeclarationBlock`.
* However, unknown `AtRule`s (like `@font-face`) are rule sets as well.
*
* If you want to manipulate a `RuleSet`, use the methods `addRule(Rule $rule)`, `getRules()` and `removeRule($rule)`
* (which accepts either a `Rule` or a rule name; optionally suffixed by a dash to remove all related rules).
*/
abstract class RuleSet implements Renderable, Commentable
{
@@ -266,23 +271,24 @@ abstract class RuleSet implements Renderable, Commentable
/**
* @return string
*/
public function render(OutputFormat $oOutputFormat)
protected function renderRules(OutputFormat $oOutputFormat)
{
$sResult = '';
$bIsFirst = true;
$oNextLevel = $oOutputFormat->nextLevel();
foreach ($this->aRules as $aRules) {
foreach ($aRules as $oRule) {
$sRendered = $oOutputFormat->safely(function () use ($oRule, $oOutputFormat) {
return $oRule->render($oOutputFormat->nextLevel());
$sRendered = $oNextLevel->safely(function () use ($oRule, $oNextLevel) {
return $oRule->render($oNextLevel);
});
if ($sRendered === null) {
continue;
}
if ($bIsFirst) {
$bIsFirst = false;
$sResult .= $oOutputFormat->nextLevel()->spaceBeforeRules();
$sResult .= $oNextLevel->spaceBeforeRules();
} else {
$sResult .= $oOutputFormat->nextLevel()->spaceBetweenRules();
$sResult .= $oNextLevel->spaceBetweenRules();
}
$sResult .= $sRendered;
}

View File

@@ -11,7 +11,8 @@ class Settings
{
/**
* Multi-byte string support.
* If true (mbstring extension must be enabled), will use (slower) `mb_strlen`, `mb_convert_case`, `mb_substr`
*
* 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
@@ -19,15 +20,14 @@ class Settings
public $bMultibyteSupport;
/**
* The default charset for the CSS if no `@charset` rule is found. Defaults to utf-8.
* The default charset for the CSS if no `@charset` declaration 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.
* Whether the parser silently ignore invalid rules instead of choking on them.
*
* @var bool
*/
@@ -47,6 +47,11 @@ class Settings
}
/**
* Enables/disables 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.
*
* @param bool $bMultibyteSupport
*
* @return self fluent interface
@@ -58,6 +63,8 @@ class Settings
}
/**
* Sets the charset to be used if the CSS does not contain an `@charset` declaration.
*
* @param string $sDefaultCharset
*
* @return self fluent interface
@@ -69,6 +76,8 @@ class Settings
}
/**
* Configures whether the parser should silently ignore invalid rules.
*
* @param bool $bLenientParsing
*
* @return self fluent interface
@@ -80,6 +89,8 @@ class Settings
}
/**
* Configures the parser to choke on invalid rules.
*
* @return self fluent interface
*/
public function beStrict()

View File

@@ -3,7 +3,12 @@
namespace Sabberworm\CSS\Value;
use Sabberworm\CSS\OutputFormat;
use Sabberworm\CSS\Parsing\ParserState;
/**
* A `CSSFunction` represents a special kind of value that also contains a function name and where the values are the
* functions arguments. It also handles equals-sign-separated argument lists like `filter: alpha(opacity=90);`.
*/
class CSSFunction extends ValueList
{
/**
@@ -28,6 +33,26 @@ class CSSFunction extends ValueList
parent::__construct($aArguments, $sSeparator, $iLineNo);
}
/**
* @param ParserState $oParserState
* @param bool $bIgnoreCase
*
* @return CSSFunction
*
* @throws SourceException
* @throws UnexpectedEOFException
* @throws UnexpectedTokenException
*/
public static function parse(ParserState $oParserState, $bIgnoreCase = false)
{
$mResult = $oParserState->parseIdentifier($bIgnoreCase);
$oParserState->consume('(');
$aArguments = Value::parseValue($oParserState, ['=', ' ', ',']);
$mResult = new CSSFunction($mResult, $aArguments, ',', $oParserState->currentLine());
$oParserState->consume(')');
return $mResult;
}
/**
* @return string
*/

View File

@@ -8,6 +8,11 @@ use Sabberworm\CSS\Parsing\SourceException;
use Sabberworm\CSS\Parsing\UnexpectedEOFException;
use Sabberworm\CSS\Parsing\UnexpectedTokenException;
/**
* This class is a wrapper for quoted strings to distinguish them from keywords.
*
* `CSSString`s always output with double quotes.
*/
class CSSString extends PrimitiveValue
{
/**

View File

@@ -10,29 +10,48 @@ class CalcFunction extends CSSFunction
{
/**
* @var int
*
* @internal
*/
const T_OPERAND = 1;
/**
* @var int
*
* @internal
*/
const T_OPERATOR = 2;
/**
* @param ParserState $oParserState
* @param bool $bIgnoreCase
*
* @return CalcFunction
*
* @throws UnexpectedTokenException
* @throws UnexpectedEOFException
*/
public static function parse(ParserState $oParserState)
public static function parse(ParserState $oParserState, $bIgnoreCase = false)
{
$aOperators = ['+', '-', '*', '/'];
$sFunction = trim($oParserState->consumeUntil('(', false, true));
$sFunction = $oParserState->parseIdentifier();
if ($oParserState->peek() != '(') {
// Found ; or end of line before an opening bracket
throw new UnexpectedTokenException('(', $oParserState->peek(), 'literal', $oParserState->currentLine());
} elseif (!in_array($sFunction, ['calc', '-moz-calc', '-webkit-calc'])) {
// Found invalid calc definition. Example calc (...
throw new UnexpectedTokenException('calc', $sFunction, 'literal', $oParserState->currentLine());
}
$oParserState->consume('(');
$oCalcList = new CalcRuleValueList($oParserState->currentLine());
$oList = new RuleValueList(',', $oParserState->currentLine());
$iNestingLevel = 0;
$iLastComponentType = null;
while (!$oParserState->comes(')') || $iNestingLevel > 0) {
if ($oParserState->isEnd() && $iNestingLevel === 0) {
break;
}
$oParserState->consumeWhiteSpace();
if ($oParserState->comes('(')) {
$iNestingLevel++;
@@ -72,7 +91,7 @@ class CalcFunction extends CSSFunction
sprintf(
'Next token was expected to be an operand of type %s. Instead "%s" was found.',
implode(', ', $aOperators),
$oVal
$oParserState->peek()
),
'',
'custom',
@@ -83,7 +102,9 @@ class CalcFunction extends CSSFunction
$oParserState->consumeWhiteSpace();
}
$oList->addListComponent($oCalcList);
$oParserState->consume(')');
if (!$oParserState->isEnd()) {
$oParserState->consume(')');
}
return new CalcFunction($sFunction, $oList, ',', $oParserState->currentLine());
}
}

View File

@@ -7,6 +7,10 @@ use Sabberworm\CSS\Parsing\ParserState;
use Sabberworm\CSS\Parsing\UnexpectedEOFException;
use Sabberworm\CSS\Parsing\UnexpectedTokenException;
/**
* `Color's can be input in the form #rrggbb, #rgb or schema(val1, val2, …) but are always stored as an array of
* ('s' => val1, 'c' => val2, 'h' => val3, …) and output in the second form.
*/
class Color extends CSSFunction
{
/**
@@ -19,12 +23,15 @@ class Color extends CSSFunction
}
/**
* @param ParserState $oParserState
* @param bool $bIgnoreCase
*
* @return Color|CSSFunction
*
* @throws UnexpectedEOFException
* @throws UnexpectedTokenException
*/
public static function parse(ParserState $oParserState)
public static function parse(ParserState $oParserState, $bIgnoreCase = false)
{
$aColor = [];
if ($oParserState->comes('#')) {
@@ -49,12 +56,19 @@ class Color extends CSSFunction
$oParserState->currentLine()
),
];
} else {
} elseif ($oParserState->strlen($sValue) === 6) {
$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 {
throw new UnexpectedTokenException(
'Invalid hex color value',
$sValue,
'custom',
$oParserState->currentLine()
);
}
} else {
$sColorMode = $oParserState->parseIdentifier(true);

View File

@@ -2,6 +2,11 @@
namespace Sabberworm\CSS\Value;
/**
* This class is used to represent all multivalued rules like `font: bold 12px/3 Helvetica, Verdana, sans-serif;`
* (where the value would be a whitespace-separated list of the primitive value `bold`, a slash-separated list
* and a comma-separated list).
*/
class RuleValueList extends ValueList
{
/**

View File

@@ -7,24 +7,38 @@ use Sabberworm\CSS\Parsing\ParserState;
use Sabberworm\CSS\Parsing\UnexpectedEOFException;
use Sabberworm\CSS\Parsing\UnexpectedTokenException;
/**
* A `Size` consists of a numeric `size` value and a unit.
*/
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>
*
* @internal
*/
const ABSOLUTE_SIZE_UNITS = ['px', 'cm', 'mm', 'mozmm', 'in', 'pt', 'pc', 'vh', 'vw', 'vmin', 'vmax', 'rem'];
const ABSOLUTE_SIZE_UNITS = [
'px', 'pt', 'pc',
'cm', 'mm', 'mozmm', 'in',
'vh', 'dvh', 'svh', 'lvh',
'vw', 'vmin', 'vmax', 'rem',
];
/**
* @var array<int, string>
*
* @internal
*/
const RELATIVE_SIZE_UNITS = ['%', 'em', 'ex', 'ch', 'fr'];
/**
* @var array<int, string>
*
* @internal
*/
const NON_SIZE_UNITS = ['deg', 'grad', 'rad', 's', 'ms', 'turns', 'Hz', 'kHz'];
const NON_SIZE_UNITS = ['deg', 'grad', 'rad', 's', 'ms', 'turn', 'Hz', 'kHz'];
/**
* @var array<int, array<string, string>>|null
@@ -74,9 +88,16 @@ class Size extends PrimitiveValue
if ($oParserState->comes('-')) {
$sSize .= $oParserState->consume('-');
}
while (is_numeric($oParserState->peek()) || $oParserState->comes('.')) {
while (is_numeric($oParserState->peek()) || $oParserState->comes('.') || $oParserState->comes('e', true)) {
if ($oParserState->comes('.')) {
$sSize .= $oParserState->consume('.');
} elseif ($oParserState->comes('e', true)) {
$sLookahead = $oParserState->peek(1, 1);
if (is_numeric($sLookahead) || $sLookahead === '+' || $sLookahead === '-') {
$sSize .= $oParserState->consume(2);
} else {
break; // Reached the unit part of the number like "em" or "ex"
}
} else {
$sSize .= $oParserState->consume(1);
}

View File

@@ -8,6 +8,9 @@ use Sabberworm\CSS\Parsing\SourceException;
use Sabberworm\CSS\Parsing\UnexpectedEOFException;
use Sabberworm\CSS\Parsing\UnexpectedTokenException;
/**
* This class represents URLs in CSS. `URL`s always output in `URL("")` notation.
*/
class URL extends PrimitiveValue
{
/**
@@ -33,11 +36,21 @@ class URL extends PrimitiveValue
*/
public static function parse(ParserState $oParserState)
{
$bUseUrl = $oParserState->comes('url', true);
$oAnchor = $oParserState->anchor();
$sIdentifier = '';
for ($i = 0; $i < 3; $i++) {
$sChar = $oParserState->parseCharacter(true);
if ($sChar === null) {
break;
}
$sIdentifier .= $sChar;
}
$bUseUrl = $oParserState->streql($sIdentifier, 'url');
if ($bUseUrl) {
$oParserState->consume('url');
$oParserState->consumeWhiteSpace();
$oParserState->consume('(');
} else {
$oAnchor->backtrack();
}
$oParserState->consumeWhiteSpace();
$oResult = new URL(CSSString::parse($oParserState), $oParserState->currentLine());

View File

@@ -8,6 +8,10 @@ use Sabberworm\CSS\Parsing\UnexpectedEOFException;
use Sabberworm\CSS\Parsing\UnexpectedTokenException;
use Sabberworm\CSS\Renderable;
/**
* Abstract base class for specific classes of CSS values: `Size`, `Color`, `CSSString` and `URL`, and another
* abstract subclass `ValueList`.
*/
abstract class Value implements Renderable
{
/**
@@ -39,8 +43,9 @@ abstract class Value implements Renderable
//Build a list of delimiters and parsed values
while (
!($oParserState->comes('}') || $oParserState->comes(';') || $oParserState->comes('!')
|| $oParserState->comes(')')
|| $oParserState->comes('\\'))
|| $oParserState->comes(')')
|| $oParserState->comes('\\')
|| $oParserState->isEnd())
) {
if (count($aStack) > 0) {
$bFoundDelimiter = false;
@@ -62,23 +67,30 @@ abstract class Value implements Renderable
}
// Convert the list to list objects
foreach ($aListDelimiters as $sDelimiter) {
if (count($aStack) === 1) {
$iStackLength = count($aStack);
if ($iStackLength === 1) {
return $aStack[0];
}
$iStartPosition = null;
while (($iStartPosition = array_search($sDelimiter, $aStack, true)) !== false) {
$aNewStack = [];
for ($iStartPosition = 0; $iStartPosition < $iStackLength; ++$iStartPosition) {
if ($iStartPosition === ($iStackLength - 1) || $sDelimiter !== $aStack[$iStartPosition + 1]) {
$aNewStack[] = $aStack[$iStartPosition];
continue;
}
$iLength = 2; //Number of elements to be joined
for ($i = $iStartPosition + 2; $i < count($aStack); $i += 2, ++$iLength) {
for ($i = $iStartPosition + 3; $i < $iStackLength; $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) {
for ($i = $iStartPosition; $i - $iStartPosition < $iLength * 2; $i += 2) {
$oList->addListComponent($aStack[$i]);
}
array_splice($aStack, $iStartPosition - 1, $iLength * 2 - 1, [$oList]);
$aNewStack[] = $oList;
$iStartPosition += $iLength * 2 - 2;
}
$aStack = $aNewStack;
}
if (!isset($aStack[0])) {
throw new UnexpectedTokenException(
@@ -101,16 +113,25 @@ abstract class Value implements Renderable
*/
public static function parseIdentifierOrFunction(ParserState $oParserState, $bIgnoreCase = false)
{
$sResult = $oParserState->parseIdentifier($bIgnoreCase);
$oAnchor = $oParserState->anchor();
$mResult = $oParserState->parseIdentifier($bIgnoreCase);
if ($oParserState->comes('(')) {
$oParserState->consume('(');
$aArguments = Value::parseValue($oParserState, ['=', ' ', ',']);
$sResult = new CSSFunction($sResult, $aArguments, ',', $oParserState->currentLine());
$oParserState->consume(')');
$oAnchor->backtrack();
if ($oParserState->streql('url', $mResult)) {
$mResult = URL::parse($oParserState);
} elseif (
$oParserState->streql('calc', $mResult)
|| $oParserState->streql('-webkit-calc', $mResult)
|| $oParserState->streql('-moz-calc', $mResult)
) {
$mResult = CalcFunction::parse($oParserState);
} else {
$mResult = CSSFunction::parse($oParserState, $bIgnoreCase);
}
}
return $sResult;
return $mResult;
}
/**
@@ -133,13 +154,6 @@ abstract class Value implements Renderable
$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) {
@@ -149,7 +163,16 @@ abstract class Value implements Renderable
} elseif ($oParserState->comes("U+")) {
$oValue = self::parseUnicodeRangeValue($oParserState);
} else {
$oValue = self::parseIdentifierOrFunction($oParserState);
$sNextChar = $oParserState->peek(1);
try {
$oValue = self::parseIdentifierOrFunction($oParserState);
} catch (UnexpectedTokenException $e) {
if (\in_array($sNextChar, ['+', '-', '*', '/'], true)) {
$oValue = $oParserState->consume(1);
} else {
throw $e;
}
}
}
$oParserState->consumeWhiteSpace();
return $oValue;

View File

@@ -4,6 +4,12 @@ namespace Sabberworm\CSS\Value;
use Sabberworm\CSS\OutputFormat;
/**
* A `ValueList` represents a lists of `Value`s, separated by some separation character
* (mostly `,`, whitespace, or `/`).
*
* There are two types of `ValueList`s: `RuleValueList` and `CSSFunction`
*/
abstract class ValueList extends Value
{
/**