Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ Please also have a look at our

### Changed

- Remove `thecodingmachine/safe` dependency (#1484)
- Clean up extra whitespace in CSS selector (#1398)
- The array keys passed to `DeclarationBlock::setSelectors()` are no longer
preserved (#1407)
Expand Down
8 changes: 5 additions & 3 deletions bin/quickdump.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,17 @@

use Sabberworm\CSS\Parser;

use function Safe\file_get_contents;

/**
* This script is used for generating the examples in the README.
*/

require_once(__DIR__ . '/../vendor/autoload.php');

$source = file_get_contents('php://stdin');
/** @phpstan-ignore theCodingMachineSafe.function */
$source = \file_get_contents('php://stdin');
if ($source === false) {
throw new \RuntimeException('Unexpected error');
}
$parser = new Parser($source);

$document = $parser->parse();
Expand Down
3 changes: 1 addition & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,7 @@
"homepage": "https://www.sabberworm.com/blog/2010/6/10/php-css-parser",
"require": {
"php": "^7.2.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0",
"ext-iconv": "*",
"thecodingmachine/safe": "^1.3 || ^2.5 || ^3.3"
"ext-iconv": "*"
},
"require-dev": {
"php-parallel-lint/php-parallel-lint": "1.4.0",
Expand Down
3 changes: 3 additions & 0 deletions config/phpstan.neon
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,6 @@ parameters:
-
message: '#^Call to static method PHPUnit\\Framework\\Assert\:\:assertInstanceOf\(\) .* will always evaluate to#'
path: '../tests/'
-
identifier: theCodingMachineSafe.function
path: '../tests/'
9 changes: 6 additions & 3 deletions src/CSSList/CSSList.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,6 @@
use Sabberworm\CSS\Value\URL;
use Sabberworm\CSS\Value\Value;

use function Safe\preg_match;

/**
* 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.
Expand Down Expand Up @@ -252,7 +250,12 @@ private static function identifierIs(string $identifier, string $match): bool
return true;
}

return preg_match("/^(-\\w+-)?$match$/i", $identifier) === 1;
/** @phpstan-ignore theCodingMachineSafe.function */
$matchResult = \preg_match("/^(-\\w+-)?$match$/i", $identifier);
if ($matchResult === false) {
throw new \RuntimeException('Unexpected error');
}
return $matchResult === 1;
}

/**
Expand Down
47 changes: 36 additions & 11 deletions src/Parsing/ParserState.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,6 @@
use Sabberworm\CSS\Comment\Comment;
use Sabberworm\CSS\Settings;

use function Safe\iconv;
use function Safe\preg_match;
use function Safe\preg_split;

/**
* @internal since 8.7.0
*/
Expand Down Expand Up @@ -121,7 +117,12 @@ public function parseIdentifier(bool $ignoreCase = true): string
}
$character = null;
while (!$this->isEnd() && ($character = $this->parseCharacter(true)) !== null) {
if (preg_match('/[a-zA-Z0-9\\x{00A0}-\\x{FFFF}_-]/Sux', $character) !== 0) {
/** @phpstan-ignore theCodingMachineSafe.function */
$matchResult = \preg_match('/[a-zA-Z0-9\\x{00A0}-\\x{FFFF}_-]/Sux', $character);
if ($matchResult === false) {
throw new \RuntimeException('Unexpected error');
}
if ($matchResult !== 0) {
$result .= $character;
} else {
$result .= '\\' . $character;
Expand All @@ -145,13 +146,23 @@ public function parseCharacter(bool $isForIdentifier): ?string
if ($this->comes('\\n') || $this->comes('\\r')) {
return '';
}
if (preg_match('/[0-9a-fA-F]/Su', $this->peek()) === 0) {
/** @phpstan-ignore theCodingMachineSafe.function */
$hexMatch = \preg_match('/[0-9a-fA-F]/Su', $this->peek());
if ($hexMatch === false) {
throw new \RuntimeException('Unexpected error');
}
if ($hexMatch === 0) {
return $this->consume(1);
}
$hexCodePoint = $this->consumeExpression('/^[0-9a-fA-F]{1,6}/u', 6);
if ($this->strlen($hexCodePoint) < 6) {
// Consume whitespace after incomplete unicode escape
if (preg_match('/\\s/isSu', $this->peek()) !== 0) {
/** @phpstan-ignore theCodingMachineSafe.function */
$whitespaceMatch = \preg_match('/\\s/isSu', $this->peek());
if ($whitespaceMatch === false) {
throw new \RuntimeException('Unexpected error');
}
if ($whitespaceMatch !== 0) {
if ($this->comes('\\r\\n')) {
$this->consume(2);
} else {
Expand All @@ -165,7 +176,8 @@ public function parseCharacter(bool $isForIdentifier): ?string
$utf32EncodedCharacter .= \chr($codePoint & 0xff);
$codePoint = $codePoint >> 8;
}
return iconv('utf-32le', $this->charset, $utf32EncodedCharacter);
/** @phpstan-ignore theCodingMachineSafe.function */
return \iconv('utf-32le', $this->charset, $utf32EncodedCharacter);

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ?string typehint on this method will throw if iconv returns false

}
if ($isForIdentifier) {
$peek = \ord($this->peek());
Expand Down Expand Up @@ -205,7 +217,15 @@ public function consumeWhiteSpace(array &$comments = []): string
{
$consumed = '';
do {
while (preg_match('/\\s/isSu', $this->peek()) === 1) {
while (true) {
/** @phpstan-ignore theCodingMachineSafe.function */
$whitespaceCheck = \preg_match('/\\s/isSu', $this->peek());
if ($whitespaceCheck === false) {
throw new \RuntimeException('Unexpected error');
}
if ($whitespaceCheck !== 1) {
break;
}
$consumed .= $this->consume(1);
}
if ($this->parserSettings->usesLenientParsing()) {
Expand Down Expand Up @@ -319,7 +339,8 @@ public function consumeExpression(string $expression, ?int $maximumLength = null
{
$matches = null;
$input = ($maximumLength !== null) ? $this->peek($maximumLength) : $this->inputLeft();
if (preg_match($expression, $input, $matches, PREG_OFFSET_CAPTURE) !== 1) {
/** @phpstan-ignore theCodingMachineSafe.function */
if (\preg_match($expression, $input, $matches, PREG_OFFSET_CAPTURE) !== 1) {
throw new UnexpectedTokenException($expression, $this->peek(5), 'expression', $this->lineNumber);

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to check for false here. If this preg_match returns false then we'll already throw an exception

}

Expand Down Expand Up @@ -468,7 +489,11 @@ private function strsplit(string $string): array
{
if ($this->parserSettings->hasMultibyteSupport()) {
if ($this->streql($this->charset, 'utf-8')) {
$result = preg_split('//u', $string, -1, PREG_SPLIT_NO_EMPTY);
/** @phpstan-ignore theCodingMachineSafe.function */
$result = \preg_split('//u', $string, -1, PREG_SPLIT_NO_EMPTY);
if ($result === false) {
throw new \RuntimeException('Unexpected error');
}
} else {
$length = \mb_strlen($string, $this->charset);
$result = [];
Expand Down
12 changes: 7 additions & 5 deletions src/Property/Selector.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,6 @@
use Sabberworm\CSS\Property\Selector\SpecificityCalculator;
use Sabberworm\CSS\Renderable;

use function Safe\preg_match;
use function Safe\preg_replace;

/**
* Class representing a single CSS selector. Selectors have to be split by the comma prior to being passed into this
* class.
Expand Down Expand Up @@ -61,7 +58,11 @@ class Selector implements Renderable
public static function isValid(string $selector): bool
{
// Note: We need to use `static::` here as the constant is overridden in the `KeyframeSelector` class.
$numberOfMatches = preg_match(static::SELECTOR_VALIDATION_RX, $selector);
/** @phpstan-ignore theCodingMachineSafe.function */
$numberOfMatches = \preg_match(static::SELECTOR_VALIDATION_RX, $selector);
if ($numberOfMatches === false) {
throw new \RuntimeException('Unexpected error');
}

return $numberOfMatches === 1;
}
Expand Down Expand Up @@ -183,7 +184,8 @@ public function setSelector(string $selector): void
$hasAttribute = \strpos($selector, '[') !== false;

// Whitespace can't be adjusted within an attribute selector, as it would change its meaning
$this->selector = !$hasAttribute ? preg_replace('/\\s++/', ' ', $selector) : $selector;
/** @phpstan-ignore theCodingMachineSafe.function */
$this->selector = !$hasAttribute ? \preg_replace('/\\s++/', ' ', $selector) : $selector;

@SjorsO SjorsO Jan 27, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

preg_replace cannot return false. The dependency has preg_replace as a special case.

I think it should be fine to use the normal version of preg_replace

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, I missed that. I've pushed a commit that checks for null

}

/**
Expand Down
14 changes: 10 additions & 4 deletions src/Property/Selector/SpecificityCalculator.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@

namespace Sabberworm\CSS\Property\Selector;

use function Safe\preg_match_all;

/**
* Utility class to calculate the specificity of a CSS selector.
*
Expand Down Expand Up @@ -65,8 +63,16 @@ public static function calculate(string $selector): int
/// @todo should exclude \# as well as "#"
$matches = null;
$b = \substr_count($selector, '#');
$c = preg_match_all(self::NON_ID_ATTRIBUTES_AND_PSEUDO_CLASSES_RX, $selector, $matches);
$d = preg_match_all(self::ELEMENTS_AND_PSEUDO_ELEMENTS_RX, $selector, $matches);
/** @phpstan-ignore theCodingMachineSafe.function */
$c = \preg_match_all(self::NON_ID_ATTRIBUTES_AND_PSEUDO_CLASSES_RX, $selector, $matches);
if ($c === false) {
throw new \RuntimeException('Unexpected error');
}
/** @phpstan-ignore theCodingMachineSafe.function */
$d = \preg_match_all(self::ELEMENTS_AND_PSEUDO_ELEMENTS_RX, $selector, $matches);
if ($d === false) {
throw new \RuntimeException('Unexpected error');
}
self::$cache[$selector] = ($a * 1000) + ($b * 100) + ($c * 10) + $d;
}

Expand Down
9 changes: 6 additions & 3 deletions src/Rule/Rule.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@
use Sabberworm\CSS\Value\RuleValueList;
use Sabberworm\CSS\Value\Value;

use function Safe\preg_match;

/**
* `Rule`s just have a string key (the rule) and a 'Value'.
*
Expand Down Expand Up @@ -103,7 +101,12 @@ public static function parse(ParserState $parserState, array $commentsBeforeRule
*/
private static function listDelimiterForRule(string $rule): array
{
if (preg_match('/^font($|-)/', $rule) === 1) {
/** @phpstan-ignore theCodingMachineSafe.function */
$matchResult = \preg_match('/^font($|-)/', $rule);
if ($matchResult === false) {
throw new \RuntimeException('Unexpected error');
}
if ($matchResult === 1) {
return [',', '/', ' '];
}

Expand Down
12 changes: 9 additions & 3 deletions src/Value/CSSString.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@
use Sabberworm\CSS\Parsing\UnexpectedTokenException;
use Sabberworm\CSS\ShortClassNameProvider;

use function Safe\preg_match;

/**
* This class is a wrapper for quoted strings to distinguish them from keywords.
*
Expand Down Expand Up @@ -59,7 +57,15 @@ public static function parse(ParserState $parserState): CSSString
$content = null;
if ($quote === null) {
// Unquoted strings end in whitespace or with braces, brackets, parentheses
while (preg_match('/[\\s{}()<>\\[\\]]/isu', $parserState->peek()) === 0) {
while (true) {
/** @phpstan-ignore theCodingMachineSafe.function */
$matchResult = \preg_match('/[\\s{}()<>\\[\\]]/isu', $parserState->peek());
if ($matchResult === false) {
throw new \RuntimeException('Unexpected error');
}
if ($matchResult !== 0) {
break;
}
$result .= $parserState->parseCharacter(false);
}
} else {
Expand Down
4 changes: 2 additions & 2 deletions src/Value/CalcFunction.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@
use Sabberworm\CSS\Parsing\UnexpectedEOFException;
use Sabberworm\CSS\Parsing\UnexpectedTokenException;

use function Safe\preg_match;

class CalcFunction extends CSSFunction
{
private const T_OPERAND = 1;
Expand Down Expand Up @@ -62,7 +60,9 @@ public static function parse(ParserState $parserState, bool $ignoreCase = false)
if (\in_array($parserState->peek(), $operators, true)) {
if (($parserState->comes('-') || $parserState->comes('+'))) {
if (
/** @phpstan-ignore theCodingMachineSafe.function */
preg_match('/\\s/', $parserState->peek(1, -1)) !== 1
/** @phpstan-ignore theCodingMachineSafe.function */
|| preg_match('/\\s/', $parserState->peek(1, 1)) !== 1

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if either of these returns false we'll already throw an exception

) {
throw new UnexpectedTokenException(
Expand Down
17 changes: 11 additions & 6 deletions src/Value/Size.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,6 @@
use Sabberworm\CSS\Parsing\UnexpectedTokenException;
use Sabberworm\CSS\ShortClassNameProvider;

use function Safe\preg_match;
use function Safe\preg_replace;

/**
* A `Size` consists of a numeric `size` value and a unit.
*/
Expand Down Expand Up @@ -200,10 +197,18 @@ public function render(OutputFormat $outputFormat): string
{
$locale = \localeconv();
$decimalPoint = \preg_quote($locale['decimal_point'], '/');
$size = preg_match('/[\\d\\.]+e[+-]?\\d+/i', (string) $this->size) === 1
? preg_replace("/$decimalPoint?0+$/", '', \sprintf('%f', $this->size)) : (string) $this->size;
/** @phpstan-ignore theCodingMachineSafe.function */
$matchResult = \preg_match('/[\\d\\.]+e[+-]?\\d+/i', (string) $this->size);
if ($matchResult === false) {
throw new \RuntimeException('Unexpected error');
}
$size = $matchResult === 1
/** @phpstan-ignore theCodingMachineSafe.function */
? \preg_replace("/$decimalPoint?0+$/", '', \sprintf('%f', $this->size))
: (string) $this->size;

return preg_replace(["/$decimalPoint/", '/^(-?)0\\./'], ['.', '$1.'], $size) . ($this->unit ?? '');
/** @phpstan-ignore theCodingMachineSafe.function */
return \preg_replace(["/$decimalPoint/", '/^(-?)0\\./'], ['.', '$1.'], $size) . ($this->unit ?? '');
}

/**
Expand Down
14 changes: 9 additions & 5 deletions src/Value/Value.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@
use Sabberworm\CSS\Position\Positionable;
use Sabberworm\CSS\ShortClassNameProvider;

use function Safe\preg_match;

/**
* Abstract base class for specific classes of CSS values: `Size`, `Color`, `CSSString` and `URL`, and another
* abstract subclass `ValueList`.
Expand Down Expand Up @@ -220,9 +218,15 @@ private static function parseUnicodeRangeValue(ParserState $parserState): string
$codepointMaxLength = 13; // Max length is 2 six-digit code points + the dash(-) between them
}
$range .= $parserState->consume(1);
} while (
(\strlen($range) < $codepointMaxLength) && (preg_match('/[A-Fa-f0-9\\?-]/', $parserState->peek()) === 1)
);
if (\strlen($range) >= $codepointMaxLength) {
break;
}
/** @phpstan-ignore theCodingMachineSafe.function */
$matchResult = \preg_match('/[A-Fa-f0-9\\?-]/', $parserState->peek());
if ($matchResult === false) {
throw new \RuntimeException('Unexpected error');
}
} while ($matchResult === 1);

return "U+{$range}";
}
Expand Down
9 changes: 3 additions & 6 deletions tests/ParserTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,6 @@
use Sabberworm\CSS\Value\URL;
use Sabberworm\CSS\Value\ValueList;

use function Safe\file_get_contents;
use function Safe\opendir;

/**
* @covers \Sabberworm\CSS\Parser
*/
Expand Down Expand Up @@ -61,7 +58,7 @@ public function parseForOneDeclarationBlockReturnsDocumentWithOneDeclarationBloc
public function files(): void
{
$directory = __DIR__ . '/fixtures';
$directoryHandle = opendir($directory);
$directoryHandle = \opendir($directory);

/* This is the correct way to loop over the directory. */
while (false !== ($filename = \readdir($directoryHandle))) {
Expand All @@ -76,7 +73,7 @@ public function files(): void
// or a future test of an as-of-now missing feature
continue;
}
$parser = new Parser(file_get_contents($directory . '/' . $filename));
$parser = new Parser(\file_get_contents($directory . '/' . $filename));
self::assertNotSame('', $parser->parse()->render());
}

Expand Down Expand Up @@ -899,7 +896,7 @@ public function missingPropertyValueLenient(): void
public static function parsedStructureForFile($filename, $settings = null): Document
{
$filename = __DIR__ . "/fixtures/$filename.css";
$parser = new Parser(file_get_contents($filename), $settings);
$parser = new Parser(\file_get_contents($filename), $settings);
return $parser->parse();
}

Expand Down
Loading