From 41b62e5beb74f7a97ed41cfe0e5cedbac9d0524a Mon Sep 17 00:00:00 2001 From: Daniel Ziegenberg Date: Thu, 20 Jun 2024 11:44:54 +0200 Subject: [PATCH] [FEATURE] Add support for PSR/Log - Add PSR/Log as dependency - Add a new SimpleLogger - Use the NullLogger per default Helps with #461 Signed-off-by: Daniel Ziegenberg --- CHANGELOG.md | 2 ++ composer.json | 3 ++- src/CSSList/CSSList.php | 17 +++++++++++++-- src/CSSList/Document.php | 7 +++++-- src/OutputFormat.php | 14 +++++++++++-- src/Parser.php | 12 +++++++++-- src/Parsing/ParserState.php | 26 ++++++++++++++++++++++- src/RuleSet/DeclarationBlock.php | 9 ++++++++ src/RuleSet/RuleSet.php | 10 ++++++++- src/SimpleLogger.php | 36 ++++++++++++++++++++++++++++++++ src/Value/Value.php | 9 +++++++- 11 files changed, 133 insertions(+), 12 deletions(-) create mode 100644 src/SimpleLogger.php diff --git a/CHANGELOG.md b/CHANGELOG.md index bd9bcd3a..69de1c25 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 (#596) - Support arithmetic operators in CSS function arguments (#607) - Add support for inserting an item in a CSS list (#545) - Add a class diagram to the README (#482) diff --git a/composer.json b/composer.json index 8a55b645..aa1ba4ee 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 43350ade..0357d15f 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; @@ -60,7 +69,7 @@ public function __construct($iLineNo = 0) * @throws UnexpectedTokenException * @throws SourceException */ - public static function parseList(ParserState $oParserState, CSSList $oList): void + public static function parseList(ParserState $oParserState, CSSList $oList, ?LoggerInterface $logger = null): void { $bIsRoot = $oList instanceof Document; if (is_string($oParserState)) { @@ -372,6 +381,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 3091b0db..474da79d 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 2fcd3f0d..3d957bfd 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,10 @@ class OutputFormat */ private $iIndentationLevel = 0; - public function __construct() {} + public function __construct() + { + $this->logger = new NullLogger(); + } /** * @param string $sName @@ -237,6 +246,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 0a56608f..e572f7cc 100644 --- a/src/Parser.php +++ b/src/Parser.php @@ -6,11 +6,17 @@ 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 +29,8 @@ class Parser */ public function __construct($sText, Settings $oParserSettings = null, $iLineNo = 1) { + $this->logger = new NullLogger(); + if ($oParserSettings === null) { $oParserSettings = Settings::create(); } @@ -55,6 +63,6 @@ public function getCharset(): void */ public function parse(): Document { - 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 02c47719..43e09426 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; @@ -137,10 +145,12 @@ public function setPosition($iPosition): void 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; @@ -287,6 +297,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; @@ -294,6 +305,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); @@ -318,6 +330,14 @@ public function consumeExpression($mExpression, $iMaxLength = null): string 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); } @@ -391,6 +411,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 1e2e62ba..7fb9e7c1 100644 --- a/src/RuleSet/DeclarationBlock.php +++ b/src/RuleSet/DeclarationBlock.php @@ -104,6 +104,10 @@ public function setSelectors($mSelector, $oList = null): void 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): void $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, @@ -792,6 +800,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 db7aef68..3509082c 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..cb9af12a --- /dev/null +++ b/src/SimpleLogger.php @@ -0,0 +1,36 @@ + $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 1cc1b5c9..af0e9ebd 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; }