diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e4f999e..d5b79808 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ This project adheres to [Semantic Versioning](https://semver.org/). ## x.y.z ### Added + +- Add support for PSR/Log with a new `SimpleLogger` and use `NullLogger` per default - Add support for inserting an item in a CSS list (#545) - Add a class diagram to the README (#482) - Add support for the `dvh`, `lvh` and `svh` length units (#415) diff --git a/composer.json b/composer.json index 1b29b625..f3efe500 100644 --- a/composer.json +++ b/composer.json @@ -24,7 +24,8 @@ ], "require": { "php": ">=7.2.0", - "ext-iconv": "*" + "ext-iconv": "*", + "psr/log": "*" }, "require-dev": { "codacy/coverage": "^1.4.3", diff --git a/src/CSSList/CSSList.php b/src/CSSList/CSSList.php index b107b6ab..844411be 100644 --- a/src/CSSList/CSSList.php +++ b/src/CSSList/CSSList.php @@ -23,14 +23,21 @@ use Sabberworm\CSS\Value\URL; use Sabberworm\CSS\Value\Value; +use Psr\Log\LoggerInterface; +use Psr\Log\LoggerAwareInterface; +use Psr\Log\LoggerAwareTrait; +use Psr\Log\NullLogger; + /** * 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. * * It can also contain `Import` and `Charset` objects stemming from at-rules. */ -abstract class CSSList implements Renderable, Commentable +abstract class CSSList implements Renderable, Commentable, LoggerAwareInterface { + use LoggerAwareTrait; + /** * @var array */ @@ -51,6 +58,8 @@ abstract class CSSList implements Renderable, Commentable */ public function __construct($iLineNo = 0) { + $this->logger = new NullLogger(); + $this->aComments = []; $this->aContents = []; $this->iLineNo = $iLineNo; @@ -62,7 +71,7 @@ public function __construct($iLineNo = 0) * @throws UnexpectedTokenException * @throws SourceException */ - public static function parseList(ParserState $oParserState, CSSList $oList) + public static function parseList(ParserState $oParserState, CSSList $oList, ?LoggerInterface $logger = null) { $bIsRoot = $oList instanceof Document; if (is_string($oParserState)) { @@ -382,6 +391,10 @@ public function removeDeclarationBlockBySelector($mSelector, $bRemoveAll = false foreach ($mSelector as $iKey => &$mSel) { if (!($mSel instanceof Selector)) { if (!Selector::isValid($mSel)) { + $this->logger->error( + 'Selector did not match {rx}.', + ['rx' => Selector::SELECTOR_VALIDATION_RX] + ); throw new UnexpectedTokenException( "Selector did not match '" . Selector::SELECTOR_VALIDATION_RX . "'.", $mSel, diff --git a/src/CSSList/Document.php b/src/CSSList/Document.php index 2478b34e..17786e12 100644 --- a/src/CSSList/Document.php +++ b/src/CSSList/Document.php @@ -10,6 +10,9 @@ use Sabberworm\CSS\RuleSet\RuleSet; use Sabberworm\CSS\Value\Value; +use Psr\Log\LoggerInterface; +use Psr\Log\NullLogger; + /** * 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`). @@ -27,10 +30,10 @@ public function __construct($iLineNo = 0) /** * @throws SourceException */ - public static function parse(ParserState $oParserState): Document + public static function parse(ParserState $oParserState, ?LoggerInterface $logger = null): Document { $oDocument = new Document($oParserState->currentLine()); - CSSList::parseList($oParserState, $oDocument); + CSSList::parseList($oParserState, $oDocument, $logger ?? new NullLogger()); return $oDocument; } diff --git a/src/OutputFormat.php b/src/OutputFormat.php index 81026b2a..b7f40c0c 100644 --- a/src/OutputFormat.php +++ b/src/OutputFormat.php @@ -2,14 +2,20 @@ namespace Sabberworm\CSS; +use Psr\Log\LoggerAwareInterface; +use Psr\Log\LoggerAwareTrait; +use Psr\Log\NullLogger; + /** * Class OutputFormat * * @method OutputFormat setSemicolonAfterLastRule(bool $bSemicolonAfterLastRule) Set whether semicolons are added after * last rule. */ -class OutputFormat +class OutputFormat implements LoggerAwareInterface { + use LoggerAwareTrait; + /** * Value format: `"` means double-quote, `'` means single-quote * @@ -165,7 +171,9 @@ class OutputFormat */ private $iIndentationLevel = 0; - public function __construct() {} + public function __construct() { + $this->logger = new NullLogger(); + } /** * @param string $sName @@ -237,6 +245,7 @@ public function __call($sMethodName, array $aArguments) } elseif (method_exists(OutputFormatter::class, $sMethodName)) { return call_user_func_array([$this->getFormatter(), $sMethodName], $aArguments); } else { + $this->logger->error('Unknown OutputFormat method called: {method}', ['method' => $sMethodName]); throw new \Exception('Unknown OutputFormat method called: ' . $sMethodName); } } diff --git a/src/Parser.php b/src/Parser.php index e582cfab..25167c98 100644 --- a/src/Parser.php +++ b/src/Parser.php @@ -6,11 +6,16 @@ use Sabberworm\CSS\Parsing\ParserState; use Sabberworm\CSS\Parsing\SourceException; +use Psr\Log\LoggerAwareInterface; +use Psr\Log\LoggerAwareTrait; +use Psr\Log\NullLogger; /** * This class parses CSS from text into a data structure. */ -class Parser +class Parser implements LoggerAwareInterface { + use LoggerAwareTrait; + /** * @var ParserState */ @@ -23,6 +28,8 @@ class Parser */ public function __construct($sText, Settings $oParserSettings = null, $iLineNo = 1) { + $this->logger = new NullLogger(); + if ($oParserSettings === null) { $oParserSettings = Settings::create(); } @@ -61,6 +68,6 @@ public function getCharset() */ public function parse() { - return Document::parse($this->oParserState); + return Document::parse($this->oParserState, $this->logger); } } diff --git a/src/Parsing/ParserState.php b/src/Parsing/ParserState.php index 13b0fce8..3b92e5e8 100644 --- a/src/Parsing/ParserState.php +++ b/src/Parsing/ParserState.php @@ -6,8 +6,14 @@ use Sabberworm\CSS\Parsing\Anchor; use Sabberworm\CSS\Settings; -class ParserState +use Psr\Log\LoggerAwareInterface; +use Psr\Log\LoggerAwareTrait; +use Psr\Log\NullLogger; + +class ParserState implements LoggerAwareInterface { + use LoggerAwareTrait; + /** * @var null * @@ -58,6 +64,8 @@ class ParserState */ public function __construct($sText, Settings $oParserSettings, $iLineNo = 1) { + $this->logger = new NullLogger(); + $this->oParserSettings = $oParserSettings; $this->sText = $sText; $this->iCurrentPosition = 0; @@ -141,10 +149,12 @@ public function setPosition($iPosition) public function parseIdentifier($bIgnoreCase = true) { if ($this->isEnd()) { + $this->logger->error('Unexpected end of file while parsing identifier at line {line}', ['line' => $this->iLineNo]); throw new UnexpectedEOFException('', '', 'identifier', $this->iLineNo); } $sResult = $this->parseCharacter(true); if ($sResult === null) { + $this->logger->error('Unexpected token while parsing identifier at line {line}', ['line' => $this->iLineNo]); throw new UnexpectedTokenException($sResult, $this->peek(5), 'identifier', $this->iLineNo); } $sCharacter = null; @@ -291,6 +301,7 @@ public function consume($mValue = 1): string $iLineCount = substr_count($mValue, "\n"); $iLength = $this->strlen($mValue); if (!$this->streql($this->substr($this->iCurrentPosition, $iLength), $mValue)) { + $this->logger->error('Unexpected token "{token}" at line {line}', ['token' => $mValue, 'line' => $this->iLineNo]); throw new UnexpectedTokenException($mValue, $this->peek(max($iLength, 5)), $this->iLineNo); } $this->iLineNo += $iLineCount; @@ -298,6 +309,7 @@ public function consume($mValue = 1): string return $mValue; } else { if ($this->iCurrentPosition + $mValue > $this->iLength) { + $this->logger->error('Unexpected end of file while consuming {count} chars at line {line}', ['count' => $mValue, 'line' => $this->iLineNo]); throw new UnexpectedEOFException($mValue, $this->peek(5), 'count', $this->iLineNo); } $sResult = $this->substr($this->iCurrentPosition, $mValue); @@ -324,6 +336,10 @@ public function consumeExpression($mExpression, $iMaxLength = null) if (preg_match($mExpression, $sInput, $aMatches, PREG_OFFSET_CAPTURE) === 1) { return $this->consume($aMatches[0][0]); } + $this->logger->error( + 'Unexpected expression "{token}" instead of {expression} at line {line}', + ['token' => $this->peek(5), 'expression' =>$mExpression, 'line' => $this->iLineNo] + ); throw new UnexpectedTokenException($mExpression, $this->peek(5), 'expression', $this->iLineNo); } @@ -397,6 +413,10 @@ public function consumeUntil($aEnd, $bIncludeEnd = false, $consumeEnd = false, a } $this->iCurrentPosition = $start; + $this->logger->error( + 'Unexpected end of file while searching for one of "{end}" at line {line}', + ['end' => implode('","', $aEnd), 'line' => $this->iLineNo] + ); throw new UnexpectedEOFException( 'One of ("' . implode('","', $aEnd) . '")', $this->peek(5), diff --git a/src/RuleSet/DeclarationBlock.php b/src/RuleSet/DeclarationBlock.php index 8fb7bc0c..6143e66f 100644 --- a/src/RuleSet/DeclarationBlock.php +++ b/src/RuleSet/DeclarationBlock.php @@ -104,6 +104,10 @@ public function setSelectors($mSelector, $oList = null) if (!($mSelector instanceof Selector)) { if ($oList === null || !($oList instanceof KeyFrame)) { if (!Selector::isValid($mSelector)) { + $this->logger->error( + "Selector did not match '{regexp}'. Found: '{selector}'", + ['regexp' => Selector::SELECTOR_VALIDATION_RX, 'selector' => $mSelector] + ); throw new UnexpectedTokenException( "Selector did not match '" . Selector::SELECTOR_VALIDATION_RX . "'.", $mSelector, @@ -113,6 +117,10 @@ public function setSelectors($mSelector, $oList = null) $this->aSelectors[$iKey] = new Selector($mSelector); } else { if (!KeyframeSelector::isValid($mSelector)) { + $this->logger->error( + "Selector did not match '{regexp}'. Found: '{selector}'", + ['regexp' => KeyframeSelector::SELECTOR_VALIDATION_RX, 'selector' => $mSelector] + ); throw new UnexpectedTokenException( "Selector did not match '" . KeyframeSelector::SELECTOR_VALIDATION_RX . "'.", $mSelector, @@ -818,6 +826,7 @@ public function render(OutputFormat $oOutputFormat): string $sResult = $oOutputFormat->comments($this); if (count($this->aSelectors) === 0) { // If all the selectors have been removed, this declaration block becomes invalid + $this->logger->error("Attempt to print declaration block with missing selector at line {line}", ["line" => $this->iLineNo]); throw new OutputException("Attempt to print declaration block with missing selector", $this->iLineNo); } $sResult .= $oOutputFormat->sBeforeDeclarationBlock; diff --git a/src/RuleSet/RuleSet.php b/src/RuleSet/RuleSet.php index adb9be92..4f89b07e 100644 --- a/src/RuleSet/RuleSet.php +++ b/src/RuleSet/RuleSet.php @@ -11,6 +11,10 @@ use Sabberworm\CSS\Renderable; use Sabberworm\CSS\Rule\Rule; +use Psr\Log\LoggerAwareInterface; +use Psr\Log\LoggerAwareTrait; +use Psr\Log\NullLogger; + /** * This class is a container for individual 'Rule's. * @@ -20,8 +24,10 @@ * 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 +abstract class RuleSet implements Renderable, Commentable, LoggerAwareInterface { + use LoggerAwareTrait; + /** * @var array */ @@ -42,6 +48,8 @@ abstract class RuleSet implements Renderable, Commentable */ public function __construct($iLineNo = 0) { + $this->logger = new NullLogger(); + $this->aRules = []; $this->iLineNo = $iLineNo; $this->aComments = []; diff --git a/src/SimpleLogger.php b/src/SimpleLogger.php new file mode 100644 index 00000000..2bba076f --- /dev/null +++ b/src/SimpleLogger.php @@ -0,0 +1,35 @@ + $val) { + // check that the value can be cast to string + if (!is_array($val) && (!is_object($val) || method_exists($val, '__toString'))) { + $replace['{' . $key . '}'] = $val; + } + } + + // interpolate replacement values into the message and return + return strtr($message, $replace); + } + +} diff --git a/src/Value/Value.php b/src/Value/Value.php index 4950e35a..66e685b9 100644 --- a/src/Value/Value.php +++ b/src/Value/Value.php @@ -9,12 +9,18 @@ use Sabberworm\CSS\Value\CSSFunction; use Sabberworm\CSS\Renderable; +use Psr\Log\LoggerAwareInterface; +use Psr\Log\LoggerAwareTrait; +use Psr\Log\NullLogger; + /** * Abstract base class for specific classes of CSS values: `Size`, `Color`, `CSSString` and `URL`, and another * abstract subclass `ValueList`. */ -abstract class Value implements Renderable +abstract class Value implements Renderable, LoggerAwareInterface { + use LoggerAwareTrait; + /** * @var int */ @@ -25,6 +31,7 @@ abstract class Value implements Renderable */ public function __construct($iLineNo = 0) { + $this->logger = new NullLogger(); $this->iLineNo = $iLineNo; }