Update besar apk finance

This commit is contained in:
Wian Drs
2026-09-11 16:03:00 +07:00
parent d42483b350
commit 4ef15c22ae
897 changed files with 98056 additions and 15979 deletions
+73 -50
View File
@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace Sabberworm\CSS\Value;
use Sabberworm\CSS\OutputFormat;
@@ -15,95 +17,116 @@ use Sabberworm\CSS\Parsing\UnexpectedTokenException;
class CSSFunction extends ValueList
{
/**
* @var string
* @var non-empty-string
*
* @internal since 8.8.0
*/
protected $sName;
protected $name;
/**
* @param string $sName
* @param RuleValueList|array<int, RuleValueList|CSSFunction|CSSString|LineName|Size|URL|string> $aArguments
* @param string $sSeparator
* @param int $iLineNo
* @param non-empty-string $name
* @param RuleValueList|array<Value|string> $arguments
* @param non-empty-string $separator
* @param int<1, max>|null $lineNumber
*/
public function __construct($sName, $aArguments, $sSeparator = ',', $iLineNo = 0)
public function __construct(string $name, $arguments, string $separator = ',', ?int $lineNumber = null)
{
if ($aArguments instanceof RuleValueList) {
$sSeparator = $aArguments->getListSeparator();
$aArguments = $aArguments->getListComponents();
if ($arguments instanceof RuleValueList) {
$separator = $arguments->getListSeparator();
$arguments = $arguments->getListComponents();
}
$this->sName = $sName;
$this->setPosition($iLineNo); // TODO: redundant?
parent::__construct($aArguments, $sSeparator, $iLineNo);
$this->name = $name;
$this->setPosition($lineNumber); // TODO: redundant?
parent::__construct($arguments, $separator, $lineNumber);
}
/**
* @param ParserState $oParserState
* @param bool $bIgnoreCase
*
* @return CSSFunction
*
* @throws SourceException
* @throws UnexpectedEOFException
* @throws UnexpectedTokenException
*
* @internal since V8.8.0
*/
public static function parse(ParserState $oParserState, $bIgnoreCase = false)
public static function parse(ParserState $parserState, bool $ignoreCase = false): CSSFunction
{
$mResult = $oParserState->parseIdentifier($bIgnoreCase);
$oParserState->consume('(');
$aArguments = Value::parseValue($oParserState, ['=', ' ', ',']);
$mResult = new CSSFunction($mResult, $aArguments, ',', $oParserState->currentLine());
$oParserState->consume(')');
return $mResult;
$name = self::parseName($parserState, $ignoreCase);
$parserState->consume('(');
$arguments = self::parseArguments($parserState);
$result = new CSSFunction($name, $arguments, ',', $parserState->currentLine());
$parserState->consume(')');
return $result;
}
/**
* @return string
* @throws SourceException
* @throws UnexpectedEOFException
* @throws UnexpectedTokenException
*/
public function getName()
private static function parseName(ParserState $parserState, bool $ignoreCase = false): string
{
return $this->sName;
return $parserState->parseIdentifier($ignoreCase);
}
/**
* @param string $sName
* @return Value|string
*
* @return void
* @throws SourceException
* @throws UnexpectedEOFException
* @throws UnexpectedTokenException
*/
public function setName($sName)
private static function parseArguments(ParserState $parserState)
{
$this->sName = $sName;
return Value::parseValue($parserState, ['=', ' ', ',']);
}
/**
* @return array<int, RuleValueList|CSSFunction|CSSString|LineName|Size|URL|string>
* @return non-empty-string
*/
public function getArguments()
public function getName(): string
{
return $this->aComponents;
return $this->name;
}
/**
* @return string
* @param non-empty-string $name
*/
public function setName(string $name): void
{
$this->name = $name;
}
/**
* @return array<Value|string>
*/
public function getArguments(): array
{
return $this->components;
}
/**
* @return non-empty-string
*/
public function render(OutputFormat $outputFormat): string
{
$arguments = parent::render($outputFormat);
return "{$this->name}({$arguments})";
}
/**
* @return array<string, bool|int|float|string|array<mixed>|null>
*
* @deprecated in V8.8.0, will be removed in V9.0.0. Use `render` instead.
* @internal
*/
public function __toString()
public function getArrayRepresentation(): array
{
return $this->render(new OutputFormat());
}
/**
* @param OutputFormat|null $oOutputFormat
*
* @return string
*/
public function render($oOutputFormat)
{
$aArguments = parent::render($oOutputFormat);
return "{$this->sName}({$aArguments})";
return \array_merge(
[
'class' => 'placeholder',
'name' => $this->name,
],
parent::getArrayRepresentation()
);
}
}
+67 -59
View File
@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace Sabberworm\CSS\Value;
use Sabberworm\CSS\OutputFormat;
@@ -7,6 +9,9 @@ use Sabberworm\CSS\Parsing\ParserState;
use Sabberworm\CSS\Parsing\SourceException;
use Sabberworm\CSS\Parsing\UnexpectedEOFException;
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.
@@ -15,102 +20,105 @@ use Sabberworm\CSS\Parsing\UnexpectedTokenException;
*/
class CSSString extends PrimitiveValue
{
use ShortClassNameProvider;
/**
* @var string
*/
private $sString;
private $string;
/**
* @param string $sString
* @param int $iLineNo
* @param int<1, max>|null $lineNumber
*/
public function __construct($sString, $iLineNo = 0)
public function __construct(string $string, ?int $lineNumber = null)
{
$this->sString = $sString;
parent::__construct($iLineNo);
$this->string = $string;
parent::__construct($lineNumber);
}
/**
* @return CSSString
*
* @throws SourceException
* @throws UnexpectedEOFException
* @throws UnexpectedTokenException
*
* @internal since V8.8.0
*/
public static function parse(ParserState $oParserState)
public static function parse(ParserState $parserState): CSSString
{
$sBegin = $oParserState->peek();
$sQuote = null;
if ($sBegin === "'") {
$sQuote = "'";
} elseif ($sBegin === '"') {
$sQuote = '"';
$begin = $parserState->peek();
$quote = null;
if ($begin === "'") {
$quote = "'";
} elseif ($begin === '"') {
$quote = '"';
}
if ($sQuote !== null) {
$oParserState->consume($sQuote);
if ($quote !== null) {
$parserState->consume($quote);
}
$sResult = "";
$sContent = null;
if ($sQuote === null) {
$result = '';
if ($quote === null) {
// Unquoted strings end in whitespace or with braces, brackets, parentheses
while (!preg_match('/[\\s{}()<>\\[\\]]/isu', $oParserState->peek())) {
$sResult .= $oParserState->parseCharacter(false);
while (preg_match('/[\\s{}()<>\\[\\]]/isu', $parserState->peek()) === 0) {
$result .= $parserState->parseCharacter(false);
}
} else {
while (!$oParserState->comes($sQuote)) {
$sContent = $oParserState->parseCharacter(false);
if ($sContent === null) {
while (!$parserState->comes($quote)) {
$content = $parserState->parseCharacter(false);
if ($content === null) {
throw new SourceException(
"Non-well-formed quoted string {$oParserState->peek(3)}",
$oParserState->currentLine()
"Non-well-formed quoted string {$parserState->peek(3)}",
$parserState->currentLine()
);
}
$sResult .= $sContent;
$result .= $content;
}
$oParserState->consume($sQuote);
$parserState->consume($quote);
}
return new CSSString($sResult, $oParserState->currentLine());
return new CSSString($result, $parserState->currentLine());
}
public function setString(string $string): void
{
$this->string = $string;
}
public function getString(): string
{
return $this->string;
}
/**
* @param string $sString
* @return non-empty-string
*/
public function render(OutputFormat $outputFormat): string
{
return $outputFormat->getStringQuotingType()
. $this->escape($this->string, $outputFormat)
. $outputFormat->getStringQuotingType();
}
/**
* @return array<string, bool|int|float|string|array<mixed>|null>
*
* @return void
* @internal
*/
public function setString($sString)
public function getArrayRepresentation(): array
{
$this->sString = $sString;
return [
'class' => $this->getShortClassName(),
// We're using the term "contents" here to make the difference to the class more clear.
'contents' => $this->string,
];
}
/**
* @return string
*/
public function getString()
private function escape(string $string, OutputFormat $outputFormat): string
{
return $this->sString;
}
$charactersToEscape = '\\';
$charactersToEscape .= ($outputFormat->getStringQuotingType() === '"' ? '"' : "'");
$withEscapedQuotes = \addcslashes($string, $charactersToEscape);
/**
* @return string
*
* @deprecated in V8.8.0, will be removed in V9.0.0. Use `render` instead.
*/
public function __toString()
{
return $this->render(new OutputFormat());
}
$withNewlineEncoded = \str_replace("\n", '\\A', $withEscapedQuotes);
/**
* @param OutputFormat|null $oOutputFormat
*
* @return string
*/
public function render($oOutputFormat)
{
$sString = addslashes($this->sString);
$sString = str_replace("\n", '\A', $sString);
return $oOutputFormat->getStringQuotingType() . $sString . $oOutputFormat->getStringQuotingType();
return $withNewlineEncoded;
}
}
+61 -67
View File
@@ -1,115 +1,109 @@
<?php
declare(strict_types=1);
namespace Sabberworm\CSS\Value;
use Sabberworm\CSS\Parsing\ParserState;
use Sabberworm\CSS\Parsing\UnexpectedEOFException;
use Sabberworm\CSS\Parsing\UnexpectedTokenException;
/**
* Support for `-webkit-calc` and `-moz-calc` is deprecated in version 8.8.0, and will be removed in version 9.0.0.
*/
use function Safe\preg_match;
class CalcFunction extends CSSFunction
{
/**
* @var int
*
* @internal
*/
const T_OPERAND = 1;
private const T_OPERAND = 1;
private const T_OPERATOR = 2;
/**
* @var int
*
* @internal
*/
const T_OPERATOR = 2;
/**
* @param ParserState $oParserState
* @param bool $bIgnoreCase
*
* @return CalcFunction
*
* @throws UnexpectedTokenException
* @throws UnexpectedEOFException
*
* @internal since V8.8.0
*/
public static function parse(ParserState $oParserState, $bIgnoreCase = false)
public static function parse(ParserState $parserState, bool $ignoreCase = false): CSSFunction
{
$aOperators = ['+', '-', '*', '/'];
$sFunction = $oParserState->parseIdentifier();
if ($oParserState->peek() != '(') {
$operators = ['+', '-', '*', '/'];
$function = $parserState->parseIdentifier();
if ($parserState->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'])) {
throw new UnexpectedTokenException('(', $parserState->peek(), 'literal', $parserState->currentLine());
} elseif ($function !== 'calc') {
// Found invalid calc definition. Example calc (...
throw new UnexpectedTokenException('calc', $sFunction, 'literal', $oParserState->currentLine());
throw new UnexpectedTokenException('calc', $function, 'literal', $parserState->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) {
$parserState->consume('(');
$calcRuleValueList = new CalcRuleValueList($parserState->currentLine());
$list = new RuleValueList(',', $parserState->currentLine());
$nestingLevel = 0;
$lastComponentType = null;
while (!$parserState->comes(')') || $nestingLevel > 0) {
if ($parserState->isEnd() && $nestingLevel === 0) {
break;
}
$oParserState->consumeWhiteSpace();
if ($oParserState->comes('(')) {
$iNestingLevel++;
$oCalcList->addListComponent($oParserState->consume(1));
$oParserState->consumeWhiteSpace();
$parserState->consumeWhiteSpace();
if ($parserState->comes('(')) {
$nestingLevel++;
$calcRuleValueList->addListComponent($parserState->consume(1));
$parserState->consumeWhiteSpace();
continue;
} elseif ($oParserState->comes(')')) {
$iNestingLevel--;
$oCalcList->addListComponent($oParserState->consume(1));
$oParserState->consumeWhiteSpace();
} elseif ($parserState->comes(')')) {
$nestingLevel--;
$calcRuleValueList->addListComponent($parserState->consume(1));
$parserState->consumeWhiteSpace();
continue;
}
if ($iLastComponentType != CalcFunction::T_OPERAND) {
$oVal = Value::parsePrimitiveValue($oParserState);
$oCalcList->addListComponent($oVal);
$iLastComponentType = CalcFunction::T_OPERAND;
if ($lastComponentType !== CalcFunction::T_OPERAND) {
$value = Value::parsePrimitiveValue($parserState);
$calcRuleValueList->addListComponent($value);
$lastComponentType = CalcFunction::T_OPERAND;
} else {
if (in_array($oParserState->peek(), $aOperators)) {
if (($oParserState->comes('-') || $oParserState->comes('+'))) {
if (\in_array($parserState->peek(), $operators, true)) {
if (($parserState->comes('-') || $parserState->comes('+'))) {
if (
$oParserState->peek(1, -1) != ' '
|| !($oParserState->comes('- ')
|| $oParserState->comes('+ '))
preg_match('/\\s/', $parserState->peek(1, -1)) !== 1
|| preg_match('/\\s/', $parserState->peek(1, 1)) !== 1
) {
throw new UnexpectedTokenException(
" {$oParserState->peek()} ",
$oParserState->peek(1, -1) . $oParserState->peek(2),
" {$parserState->peek()} ",
$parserState->peek(1, -1) . $parserState->peek(2),
'literal',
$oParserState->currentLine()
$parserState->currentLine()
);
}
}
$oCalcList->addListComponent($oParserState->consume(1));
$iLastComponentType = CalcFunction::T_OPERATOR;
$calcRuleValueList->addListComponent($parserState->consume(1));
$lastComponentType = CalcFunction::T_OPERATOR;
} else {
throw new UnexpectedTokenException(
sprintf(
\sprintf(
'Next token was expected to be an operand of type %s. Instead "%s" was found.',
implode(', ', $aOperators),
$oParserState->peek()
\implode(', ', $operators),
$parserState->peek()
),
'',
'custom',
$oParserState->currentLine()
$parserState->currentLine()
);
}
}
$oParserState->consumeWhiteSpace();
$parserState->consumeWhiteSpace();
}
$oList->addListComponent($oCalcList);
if (!$oParserState->isEnd()) {
$oParserState->consume(')');
$list->addListComponent($calcRuleValueList);
if (!$parserState->isEnd()) {
$parserState->consume(')');
}
return new CalcFunction($sFunction, $oList, ',', $oParserState->currentLine());
return new CalcFunction($function, $list, ',', $parserState->currentLine());
}
/**
* @return array<string, bool|int|float|string|array<mixed>|null>
*
* @internal
*/
public function getArrayRepresentation(): array
{
throw new \BadMethodCallException('`getArrayRepresentation` is not yet implemented for `' . self::class . '`');
}
}
@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace Sabberworm\CSS\Value;
use Sabberworm\CSS\OutputFormat;
@@ -7,20 +9,25 @@ use Sabberworm\CSS\OutputFormat;
class CalcRuleValueList extends RuleValueList
{
/**
* @param int $iLineNo
* @param int<1, max>|null $lineNumber
*/
public function __construct($iLineNo = 0)
public function __construct(?int $lineNumber = null)
{
parent::__construct(',', $iLineNo);
parent::__construct(',', $lineNumber);
}
public function render(OutputFormat $outputFormat): string
{
return $outputFormat->getFormatter()->implode(' ', $this->components);
}
/**
* @param OutputFormat|null $oOutputFormat
* @return array<string, bool|int|float|string|array<mixed>|null>
*
* @return string
* @internal
*/
public function render($oOutputFormat)
public function getArrayRepresentation(): array
{
return $oOutputFormat->implode(' ', $this->aComponents);
throw new \BadMethodCallException('`getArrayRepresentation` is not yet implemented for `' . self::class . '`');
}
}
+331 -117
View File
@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace Sabberworm\CSS\Value;
use Sabberworm\CSS\OutputFormat;
@@ -14,173 +16,385 @@ use Sabberworm\CSS\Parsing\UnexpectedTokenException;
class Color extends CSSFunction
{
/**
* @param array<int, RuleValueList|CSSFunction|CSSString|LineName|Size|URL|string> $aColor
* @param int $iLineNo
* @param array<non-empty-string, Value|string> $colorValues
* @param int<1, max>|null $lineNumber
*/
public function __construct(array $aColor, $iLineNo = 0)
public function __construct(array $colorValues, ?int $lineNumber = null)
{
parent::__construct(implode('', array_keys($aColor)), $aColor, ',', $iLineNo);
parent::__construct(\implode('', \array_keys($colorValues)), $colorValues, ',', $lineNumber);
}
/**
* @param ParserState $oParserState
* @param bool $bIgnoreCase
*
* @return Color|CSSFunction
*
* @throws UnexpectedEOFException
* @throws UnexpectedTokenException
*
* @internal since V8.8.0
*/
public static function parse(ParserState $oParserState, $bIgnoreCase = false)
public static function parse(ParserState $parserState, bool $ignoreCase = false): CSSFunction
{
$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];
}
return $parserState->comes('#')
? self::parseHexColor($parserState)
: self::parseColorFunction($parserState);
}
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()
),
];
} 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()
);
}
/**
* @throws UnexpectedEOFException
* @throws UnexpectedTokenException
*/
private static function parseHexColor(ParserState $parserState): Color
{
$parserState->consume('#');
$hexValue = $parserState->parseIdentifier(false);
if ($parserState->strlen($hexValue) === 3) {
$hexValue = $hexValue[0] . $hexValue[0] . $hexValue[1] . $hexValue[1] . $hexValue[2] . $hexValue[2];
} elseif ($parserState->strlen($hexValue) === 4) {
$hexValue = $hexValue[0] . $hexValue[0] . $hexValue[1] . $hexValue[1] . $hexValue[2] . $hexValue[2]
. $hexValue[3] . $hexValue[3];
}
if ($parserState->strlen($hexValue) === 8) {
$colorValues = [
'r' => new Size(\intval($hexValue[0] . $hexValue[1], 16), null, true, $parserState->currentLine()),
'g' => new Size(\intval($hexValue[2] . $hexValue[3], 16), null, true, $parserState->currentLine()),
'b' => new Size(\intval($hexValue[4] . $hexValue[5], 16), null, true, $parserState->currentLine()),
'a' => new Size(
\round(self::mapRange(\intval($hexValue[6] . $hexValue[7], 16), 0, 255, 0, 1), 2),
null,
true,
$parserState->currentLine()
),
];
} elseif ($parserState->strlen($hexValue) === 6) {
$colorValues = [
'r' => new Size(\intval($hexValue[0] . $hexValue[1], 16), null, true, $parserState->currentLine()),
'g' => new Size(\intval($hexValue[2] . $hexValue[3], 16), null, true, $parserState->currentLine()),
'b' => new Size(\intval($hexValue[4] . $hexValue[5], 16), null, true, $parserState->currentLine()),
];
} else {
$sColorMode = $oParserState->parseIdentifier(true);
$oParserState->consumeWhiteSpace();
$oParserState->consume('(');
throw new UnexpectedTokenException(
'Invalid hex color value',
$hexValue,
'custom',
$parserState->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);
}
return new Color($colorValues, $parserState->currentLine());
}
if ($bContainsVar && $oParserState->comes(')')) {
// With a var argument the function can have fewer arguments
break;
}
/**
* @throws UnexpectedEOFException
* @throws UnexpectedTokenException
*/
private static function parseColorFunction(ParserState $parserState): CSSFunction
{
$colorValues = [];
$oParserState->consumeWhiteSpace();
if ($i < ($iLength - 1)) {
$oParserState->consume(',');
}
$colorMode = $parserState->parseIdentifier(true);
$parserState->consumeWhiteSpace();
$parserState->consume('(');
// CSS Color Module Level 4 says that `rgb` and `rgba` are now aliases; likewise `hsl` and `hsla`.
// So, attempt to parse with the `a`, and allow for it not being there.
switch ($colorMode) {
case 'rgb':
$colorModeForParsing = 'rgba';
$mayHaveOptionalAlpha = true;
break;
case 'hsl':
$colorModeForParsing = 'hsla';
$mayHaveOptionalAlpha = true;
break;
case 'rgba':
// This is handled identically to the following case.
case 'hsla':
$colorModeForParsing = $colorMode;
$mayHaveOptionalAlpha = true;
break;
default:
$colorModeForParsing = $colorMode;
$mayHaveOptionalAlpha = false;
}
$containsVar = false;
$containsNone = false;
$isLegacySyntax = false;
$expectedArgumentCount = $parserState->strlen($colorModeForParsing);
for ($argumentIndex = 0; $argumentIndex < $expectedArgumentCount; ++$argumentIndex) {
$parserState->consumeWhiteSpace();
$valueKey = $colorModeForParsing[$argumentIndex];
if ($parserState->comes('var')) {
$colorValues[$valueKey] = CSSFunction::parseIdentifierOrFunction($parserState);
$containsVar = true;
} elseif (!$isLegacySyntax && $parserState->comes('none')) {
$colorValues[$valueKey] = $parserState->parseIdentifier();
$containsNone = true;
} else {
$colorValues[$valueKey] = Size::parse($parserState, true);
}
$oParserState->consume(')');
if ($bContainsVar) {
return new CSSFunction($sColorMode, array_values($aColor), ',', $oParserState->currentLine());
// This must be done first, to consume comments as well, so that the `comes` test will work.
$parserState->consumeWhiteSpace();
// With a `var` argument, the function can have fewer arguments.
// And as of CSS Color Module Level 4, the alpha argument is optional.
$canCloseNow
= $containsVar
|| ($mayHaveOptionalAlpha && $argumentIndex >= $expectedArgumentCount - 2);
if ($canCloseNow && $parserState->comes(')')) {
break;
}
// "Legacy" syntax is comma-delimited, and does not allow the `none` keyword.
// "Modern" syntax is space-delimited, with `/` as alpha delimiter.
// They cannot be mixed.
if ($argumentIndex === 0 && !$containsNone) {
// An immediate closing parenthesis is not valid.
if ($parserState->comes(')')) {
throw new UnexpectedTokenException(
'Color function with no arguments',
'',
'custom',
$parserState->currentLine()
);
}
$isLegacySyntax = $parserState->comes(',');
}
if ($isLegacySyntax && $argumentIndex < ($expectedArgumentCount - 1)) {
$parserState->consume(',');
}
// In the "modern" syntax, the alpha value must be delimited with `/`.
if (!$isLegacySyntax) {
if ($containsVar) {
// If the `var` substitution encompasses more than one argument,
// the alpha deliminator may come at any time.
if ($parserState->comes('/')) {
$parserState->consume('/');
}
} elseif (($colorModeForParsing[$argumentIndex + 1] ?? '') === 'a') {
// Alpha value is the next expected argument.
// Since a closing parenthesis was not found, a `/` separator is now required.
$parserState->consume('/');
}
}
}
return new Color($aColor, $oParserState->currentLine());
$parserState->consume(')');
return $containsVar
? new CSSFunction($colorMode, \array_values($colorValues), ',', $parserState->currentLine())
: new Color($colorValues, $parserState->currentLine());
}
/**
* @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)
private static function mapRange(float $value, float $fromMin, float $fromMax, float $toMin, float $toMax): float
{
$fFromRange = $fFromMax - $fFromMin;
$fToRange = $fToMax - $fToMin;
$fMultiplier = $fToRange / $fFromRange;
$fNewVal = $fVal - $fFromMin;
$fNewVal *= $fMultiplier;
return $fNewVal + $fToMin;
$fromRange = $fromMax - $fromMin;
$toRange = $toMax - $toMin;
$multiplier = $toRange / $fromRange;
$newValue = $value - $fromMin;
$newValue *= $multiplier;
return $newValue + $toMin;
}
/**
* @return array<int, RuleValueList|CSSFunction|CSSString|LineName|Size|URL|string>
* @return array<non-empty-string, Value|string>
*/
public function getColor()
public function getColor(): array
{
return $this->aComponents;
return $this->components;
}
/**
* @param array<int, RuleValueList|CSSFunction|CSSString|LineName|Size|URL|string> $aColor
*
* @return void
* @param array<non-empty-string, Value|string> $colorValues
*/
public function setColor(array $aColor)
public function setColor(array $colorValues): void
{
$this->setName(implode('', array_keys($aColor)));
$this->aComponents = $aColor;
$this->setName(\implode('', \array_keys($colorValues)));
$this->components = $colorValues;
}
/**
* @return string
* @return non-empty-string
*/
public function getColorDescription()
public function getColorDescription(): string
{
return $this->getName();
}
/**
* @return string
*
* @deprecated in V8.8.0, will be removed in V9.0.0. Use `render` instead.
* @return non-empty-string
*/
public function __toString()
public function render(OutputFormat $outputFormat): string
{
return $this->render(new OutputFormat());
if ($this->shouldRenderAsHex($outputFormat)) {
return $this->renderAsHex();
}
if ($this->shouldRenderInModernSyntax()) {
return $this->renderInModernSyntax($outputFormat);
}
return parent::render($outputFormat);
}
/**
* @param OutputFormat|null $oOutputFormat
* @return array<string, bool|int|float|string|array<mixed>|null>
*
* @return string
* @internal
*/
public function render($oOutputFormat)
public function getArrayRepresentation(): array
{
// 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);
throw new \BadMethodCallException('`getArrayRepresentation` is not yet implemented for `' . self::class . '`');
}
private function shouldRenderAsHex(OutputFormat $outputFormat): bool
{
return
$outputFormat->usesRgbHashNotation()
&& $this->getRealName() === 'rgb'
&& $this->allComponentsAreNumbers();
}
/**
* The function name is a concatenation of the array keys of the components, which is passed to the constructor.
* However, this can be changed by calling {@see CSSFunction::setName},
* so is not reliable in situations where it's necessary to determine the function name based on the components.
*/
private function getRealName(): string
{
return \implode('', \array_keys($this->components));
}
/**
* Test whether all color components are absolute numbers (CSS type `number`), not percentages or anything else.
* If any component is not an instance of `Size`, the method will also return `false`.
*/
private function allComponentsAreNumbers(): bool
{
foreach ($this->components as $component) {
if (!($component instanceof Size) || $component->getUnit() !== null) {
return false;
}
}
return parent::render($oOutputFormat);
return true;
}
/**
* Note that this method assumes the following:
* - The `components` array has keys for `r`, `g` and `b`;
* - The values in the array are all instances of `Size`.
*
* Errors will be triggered or thrown if this is not the case.
*
* @return non-empty-string
*/
private function renderAsHex(): string
{
$result = \sprintf(
'%02x%02x%02x',
$this->components['r']->getSize(),
$this->components['g']->getSize(),
$this->components['b']->getSize()
);
$canUseShortVariant = ($result[0] === $result[1]) && ($result[2] === $result[3]) && ($result[4] === $result[5]);
return '#' . ($canUseShortVariant ? $result[0] . $result[2] . $result[4] : $result);
}
/**
* The "legacy" syntax does not allow RGB colors to have a mixture of `percentage`s and `number`s,
* and does not allow `none` as any component value.
*
* The "legacy" and "modern" monikers are part of the formal W3C syntax.
* See the following for more information:
* - {@link
* https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/rgb#formal_syntax
* Description of the formal syntax for `rgb()` on MDN
* };
* - {@link
* https://www.w3.org/TR/css-color-4/#rgb-functions
* The same in the CSS Color Module Level 4 W3C Candidate Recommendation Draft
* } (as of 13 February 2024, at time of writing).
*/
private function shouldRenderInModernSyntax(): bool
{
if ($this->hasNoneAsComponentValue()) {
return true;
}
if (!$this->colorFunctionMayHaveMixedValueTypes($this->getRealName())) {
return false;
}
$hasPercentage = false;
$hasNumber = false;
foreach ($this->components as $key => $value) {
if ($key === 'a') {
// Alpha can have units that don't match those of the RGB components in the "legacy" syntax.
// So it is not necessary to check it. It's also always last, hence `break` rather than `continue`.
break;
}
if (!($value instanceof Size)) {
// Unexpected, unknown, or modified via the API
return false;
}
$unit = $value->getUnit();
// `switch` only does loose comparison
if ($unit === null) {
$hasNumber = true;
} elseif ($unit === '%') {
$hasPercentage = true;
} else {
// Invalid unit
return false;
}
}
return $hasPercentage && $hasNumber;
}
private function hasNoneAsComponentValue(): bool
{
return \in_array('none', $this->components, true);
}
/**
* Some color functions, such as `rgb`,
* may have a mixture of `percentage`, `number`, or possibly other types in their arguments.
*
* Note that this excludes the alpha component, which is treated separately.
*/
private function colorFunctionMayHaveMixedValueTypes(string $function): bool
{
$functionsThatMayHaveMixedValueTypes = ['rgb', 'rgba'];
return \in_array($function, $functionsThatMayHaveMixedValueTypes, true);
}
/**
* @return non-empty-string
*/
private function renderInModernSyntax(OutputFormat $outputFormat): string
{
// Maybe not yet without alpha, but will be...
$componentsWithoutAlpha = $this->components;
\end($componentsWithoutAlpha);
if (\key($componentsWithoutAlpha) === 'a') {
$alpha = $this->components['a'];
unset($componentsWithoutAlpha['a']);
}
$formatter = $outputFormat->getFormatter();
$arguments = $formatter->implode(' ', $componentsWithoutAlpha);
if (isset($alpha)) {
$separator = $formatter->spaceBeforeListArgumentSeparator('/')
. '/' . $formatter->spaceAfterListArgumentSeparator('/');
$arguments = $formatter->implode($separator, [$arguments, $alpha]);
}
return $this->getName() . '(' . $arguments . ')';
}
}
+26 -32
View File
@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace Sabberworm\CSS\Value;
use Sabberworm\CSS\OutputFormat;
@@ -7,64 +9,56 @@ use Sabberworm\CSS\Parsing\ParserState;
use Sabberworm\CSS\Parsing\UnexpectedEOFException;
use Sabberworm\CSS\Parsing\UnexpectedTokenException;
/**
* A name for a named CSS grid line.
*
* @see https://www.w3.org/TR/css-grid-1/#line-name
* @see https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Grid_layout/Named_grid_lines
*/
class LineName extends ValueList
{
/**
* @param array<int, RuleValueList|CSSFunction|CSSString|LineName|Size|URL|string> $aComponents
* @param int $iLineNo
* @param array<string> $components
* @param int<1, max>|null $lineNumber
*/
public function __construct(array $aComponents = [], $iLineNo = 0)
public function __construct(array $components = [], ?int $lineNumber = null)
{
parent::__construct($aComponents, ' ', $iLineNo);
parent::__construct($components, ' ', $lineNumber);
}
/**
* @return LineName
*
* @throws UnexpectedTokenException
* @throws UnexpectedEOFException
*
* @internal since V8.8.0
*/
public static function parse(ParserState $oParserState)
public static function parse(ParserState $parserState): LineName
{
$oParserState->consume('[');
$oParserState->consumeWhiteSpace();
$aNames = [];
$parserState->consume('[');
$parserState->consumeWhiteSpace();
$names = [];
do {
if ($oParserState->getSettings()->bLenientParsing) {
if ($parserState->getSettings()->usesLenientParsing()) {
try {
$aNames[] = $oParserState->parseIdentifier();
$names[] = $parserState->parseIdentifier();
} catch (UnexpectedTokenException $e) {
if (!$oParserState->comes(']')) {
if (!$parserState->comes(']')) {
throw $e;
}
}
} else {
$aNames[] = $oParserState->parseIdentifier();
$names[] = $parserState->parseIdentifier();
}
$oParserState->consumeWhiteSpace();
} while (!$oParserState->comes(']'));
$oParserState->consume(']');
return new LineName($aNames, $oParserState->currentLine());
$parserState->consumeWhiteSpace();
} while (!$parserState->comes(']'));
$parserState->consume(']');
return new LineName($names, $parserState->currentLine());
}
/**
* @return string
*
* @deprecated in V8.8.0, will be removed in V9.0.0. Use `render` instead.
* @return non-empty-string
*/
public function __toString()
{
return $this->render(new OutputFormat());
}
/**
* @param OutputFormat|null $oOutputFormat
*
* @return string
*/
public function render($oOutputFormat)
public function render(OutputFormat $outputFormat): string
{
return '[' . parent::render(OutputFormat::createCompact()) . ']';
}
@@ -1,14 +1,7 @@
<?php
declare(strict_types=1);
namespace Sabberworm\CSS\Value;
abstract class PrimitiveValue extends Value
{
/**
* @param int $iLineNo
*/
public function __construct($iLineNo = 0)
{
parent::__construct($iLineNo);
}
}
abstract class PrimitiveValue extends Value {}
@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace Sabberworm\CSS\Value;
/**
@@ -10,11 +12,11 @@ namespace Sabberworm\CSS\Value;
class RuleValueList extends ValueList
{
/**
* @param string $sSeparator
* @param int $iLineNo
* @param non-empty-string $separator
* @param int<1, max>|null $lineNumber
*/
public function __construct($sSeparator = ',', $iLineNo = 0)
public function __construct(string $separator = ',', ?int $lineNumber = null)
{
parent::__construct([], $sSeparator, $iLineNo);
parent::__construct([], $separator, $lineNumber);
}
}
+91 -115
View File
@@ -1,25 +1,29 @@
<?php
declare(strict_types=1);
namespace Sabberworm\CSS\Value;
use Sabberworm\CSS\OutputFormat;
use Sabberworm\CSS\Parsing\ParserState;
use Sabberworm\CSS\Parsing\UnexpectedEOFException;
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.
*/
class Size extends PrimitiveValue
{
use ShortClassNameProvider;
/**
* vh/vw/vm(ax)/vmin/rem are absolute insofar as they don’t scale to the immediate parent (only the viewport)
*
* @var array<int, string>
*
* @internal
*/
const ABSOLUTE_SIZE_UNITS = [
private const ABSOLUTE_SIZE_UNITS = [
'px',
'pt',
'pc',
@@ -37,211 +41,183 @@ class Size extends PrimitiveValue
'rem',
];
/**
* @var array<int, string>
*
* @internal
*/
const RELATIVE_SIZE_UNITS = ['%', 'em', 'ex', 'ch', 'fr'];
private const RELATIVE_SIZE_UNITS = ['%', 'em', 'ex', 'ch', 'fr'];
private const NON_SIZE_UNITS = ['deg', 'grad', 'rad', 's', 'ms', 'turn', 'Hz', 'kHz'];
/**
* @var array<int, string>
*
* @internal
*/
const NON_SIZE_UNITS = ['deg', 'grad', 'rad', 's', 'ms', 'turn', 'Hz', 'kHz'];
/**
* @var array<int, array<string, string>>|null
* @var array<int<1, max>, array<lowercase-string, non-empty-string>>|null
*/
private static $SIZE_UNITS = null;
/**
* @var float
*/
private $fSize;
private $size;
/**
* @var string|null
*/
private $sUnit;
private $unit;
/**
* @var bool
*/
private $bIsColorComponent;
private $isColorComponent;
/**
* @param float|int|string $fSize
* @param string|null $sUnit
* @param bool $bIsColorComponent
* @param int $iLineNo
* @param float|int|string $size
* @param int<1, max>|null $lineNumber
*/
public function __construct($fSize, $sUnit = null, $bIsColorComponent = false, $iLineNo = 0)
public function __construct($size, ?string $unit = null, bool $isColorComponent = false, ?int $lineNumber = null)
{
parent::__construct($iLineNo);
$this->fSize = (float)$fSize;
$this->sUnit = $sUnit;
$this->bIsColorComponent = $bIsColorComponent;
parent::__construct($lineNumber);
$this->size = (float) $size;
$this->unit = $unit;
$this->isColorComponent = $isColorComponent;
}
/**
* @param bool $bIsColorComponent
*
* @return Size
*
* @throws UnexpectedEOFException
* @throws UnexpectedTokenException
*
* @internal since V8.8.0
*/
public static function parse(ParserState $oParserState, $bIsColorComponent = false)
public static function parse(ParserState $parserState, bool $isColorComponent = false): Size
{
$sSize = '';
if ($oParserState->comes('-')) {
$sSize .= $oParserState->consume('-');
$size = '';
if ($parserState->comes('-')) {
$size .= $parserState->consume('-');
}
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);
while (\is_numeric($parserState->peek()) || $parserState->comes('.') || $parserState->comes('e', true)) {
if ($parserState->comes('.')) {
$size .= $parserState->consume('.');
} elseif ($parserState->comes('e', true)) {
$lookahead = $parserState->peek(1, 1);
if (\is_numeric($lookahead) || $lookahead === '+' || $lookahead === '-') {
$size .= $parserState->consume(2);
} else {
break; // Reached the unit part of the number like "em" or "ex"
}
} else {
$sSize .= $oParserState->consume(1);
$size .= $parserState->consume(1);
}
}
$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);
$unit = null;
$sizeUnits = self::getSizeUnits();
foreach ($sizeUnits as $length => &$values) {
$key = \strtolower($parserState->peek($length));
if (\array_key_exists($key, $values)) {
if (($unit = $values[$key]) !== null) {
$parserState->consume($length);
break;
}
}
}
return new Size((float)$sSize, $sUnit, $bIsColorComponent, $oParserState->currentLine());
return new Size((float) $size, $unit, $isColorComponent, $parserState->currentLine());
}
/**
* @return array<int, array<string, string>>
* @return array<int<1, max>, array<lowercase-string, non-empty-string>>
*/
private static function getSizeUnits()
private static function getSizeUnits(): array
{
if (!is_array(self::$SIZE_UNITS)) {
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] = [];
$sizeUnits = \array_merge(self::ABSOLUTE_SIZE_UNITS, self::RELATIVE_SIZE_UNITS, self::NON_SIZE_UNITS);
foreach ($sizeUnits as $sizeUnit) {
$tokenLength = \strlen($sizeUnit);
if (!isset(self::$SIZE_UNITS[$tokenLength])) {
self::$SIZE_UNITS[$tokenLength] = [];
}
self::$SIZE_UNITS[$iSize][strtolower($val)] = $val;
self::$SIZE_UNITS[$tokenLength][\strtolower($sizeUnit)] = $sizeUnit;
}
krsort(self::$SIZE_UNITS, SORT_NUMERIC);
\krsort(self::$SIZE_UNITS, SORT_NUMERIC);
}
return self::$SIZE_UNITS;
}
/**
* @param string $sUnit
*
* @return void
*/
public function setUnit($sUnit)
public function setUnit(string $unit): void
{
$this->sUnit = $sUnit;
$this->unit = $unit;
}
public function getUnit(): ?string
{
return $this->unit;
}
/**
* @return string|null
* @param float|int|string $size
*/
public function getUnit()
public function setSize($size): void
{
return $this->sUnit;
$this->size = (float) $size;
}
/**
* @param float|int|string $fSize
*/
public function setSize($fSize)
public function getSize(): float
{
$this->fSize = (float)$fSize;
return $this->size;
}
/**
* @return float
*/
public function getSize()
public function isColorComponent(): bool
{
return $this->fSize;
}
/**
* @return bool
*/
public function isColorComponent()
{
return $this->bIsColorComponent;
return $this->isColorComponent;
}
/**
* 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.
* Returns `false` if the unit is an angle, a duration, a frequency, or the number is a component in a `Color`
* object.
*/
public function isSize()
public function isSize(): bool
{
if (in_array($this->sUnit, self::NON_SIZE_UNITS, true)) {
if (\in_array($this->unit, self::NON_SIZE_UNITS, true)) {
return false;
}
return !$this->isColorComponent();
}
/**
* @return bool
*/
public function isRelative()
public function isRelative(): bool
{
if (in_array($this->sUnit, self::RELATIVE_SIZE_UNITS, true)) {
if (\in_array($this->unit, self::RELATIVE_SIZE_UNITS, true)) {
return true;
}
if ($this->sUnit === null && $this->fSize != 0) {
if ($this->unit === null && $this->size !== 0.0) {
return true;
}
return false;
}
/**
* @return string
*
* @deprecated in V8.8.0, will be removed in V9.0.0. Use `render` instead.
* @return non-empty-string
*/
public function __toString()
public function render(OutputFormat $outputFormat): string
{
return $this->render(new OutputFormat());
$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;
return preg_replace(["/$decimalPoint/", '/^(-?)0\\./'], ['.', '$1.'], $size) . ($this->unit ?? '');
}
/**
* @param OutputFormat|null $oOutputFormat
* @return array<string, bool|int|float|string|array<mixed>|null>
*
* @return string
* @internal
*/
public function render($oOutputFormat)
public function getArrayRepresentation(): array
{
$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)) : (string)$this->fSize;
return preg_replace(["/$sPoint/", "/^(-?)0\./"], ['.', '$1.'], $sSize)
. ($this->sUnit === null ? '' : $this->sUnit);
return [
'class' => $this->getShortClassName(),
// 'number' is the official W3C terminology (not 'size')
'number' => $this->size,
'unit' => $this->unit,
];
}
}
+48 -48
View File
@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace Sabberworm\CSS\Value;
use Sabberworm\CSS\OutputFormat;
@@ -7,95 +9,93 @@ use Sabberworm\CSS\Parsing\ParserState;
use Sabberworm\CSS\Parsing\SourceException;
use Sabberworm\CSS\Parsing\UnexpectedEOFException;
use Sabberworm\CSS\Parsing\UnexpectedTokenException;
use Sabberworm\CSS\ShortClassNameProvider;
/**
* This class represents URLs in CSS. `URL`s always output in `URL("")` notation.
*/
class URL extends PrimitiveValue
{
use ShortClassNameProvider;
/**
* @var CSSString
*/
private $oURL;
private $url;
/**
* @param int $iLineNo
* @param int<1, max>|null $lineNumber
*/
public function __construct(CSSString $oURL, $iLineNo = 0)
public function __construct(CSSString $url, ?int $lineNumber = null)
{
parent::__construct($iLineNo);
$this->oURL = $oURL;
parent::__construct($lineNumber);
$this->url = $url;
}
/**
* @return URL
*
* @throws SourceException
* @throws UnexpectedEOFException
* @throws UnexpectedTokenException
*
* @internal since V8.8.0
*/
public static function parse(ParserState $oParserState)
public static function parse(ParserState $parserState): URL
{
$oAnchor = $oParserState->anchor();
$sIdentifier = '';
$anchor = $parserState->anchor();
$identifier = '';
for ($i = 0; $i < 3; $i++) {
$sChar = $oParserState->parseCharacter(true);
if ($sChar === null) {
$character = $parserState->parseCharacter(true);
if ($character === null) {
break;
}
$sIdentifier .= $sChar;
$identifier .= $character;
}
$bUseUrl = $oParserState->streql($sIdentifier, 'url');
if ($bUseUrl) {
$oParserState->consumeWhiteSpace();
$oParserState->consume('(');
$useUrl = $parserState->streql($identifier, 'url');
if ($useUrl) {
$parserState->consumeWhiteSpace();
$parserState->consume('(');
} else {
$oAnchor->backtrack();
$anchor->backtrack();
}
$oParserState->consumeWhiteSpace();
$oResult = new URL(CSSString::parse($oParserState), $oParserState->currentLine());
if ($bUseUrl) {
$oParserState->consumeWhiteSpace();
$oParserState->consume(')');
$parserState->consumeWhiteSpace();
$result = new URL(CSSString::parse($parserState), $parserState->currentLine());
if ($useUrl) {
$parserState->consumeWhiteSpace();
$parserState->consume(')');
}
return $oResult;
return $result;
}
/**
* @return void
*/
public function setURL(CSSString $oURL)
public function setURL(CSSString $url): void
{
$this->oURL = $oURL;
$this->url = $url;
}
/**
* @return CSSString
*/
public function getURL()
public function getURL(): CSSString
{
return $this->oURL;
return $this->url;
}
/**
* @return string
* @return non-empty-string
*/
public function render(OutputFormat $outputFormat): string
{
return "url({$this->url->render($outputFormat)})";
}
/**
* @return array<string, bool|int|float|string|array<mixed>|null>
*
* @deprecated in V8.8.0, will be removed in V9.0.0. Use `render` instead.
* @internal
*/
public function __toString()
public function getArrayRepresentation(): array
{
return $this->render(new OutputFormat());
}
/**
* @param OutputFormat|null $oOutputFormat
*
* @return string
*/
public function render($oOutputFormat)
{
return "url({$this->oURL->render($oOutputFormat)})";
return [
'class' => $this->getShortClassName(),
// We're using the term "uri" here to match the wording used in the specs:
// https://www.w3.org/TR/CSS22/syndata.html#uri
'uri' => $this->url->getArrayRepresentation(),
];
}
}
+115 -105
View File
@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace Sabberworm\CSS\Value;
use Sabberworm\CSS\CSSElement;
@@ -9,6 +11,9 @@ use Sabberworm\CSS\Parsing\UnexpectedEOFException;
use Sabberworm\CSS\Parsing\UnexpectedTokenException;
use Sabberworm\CSS\Position\Position;
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
@@ -17,96 +22,94 @@ use Sabberworm\CSS\Position\Positionable;
abstract class Value implements CSSElement, Positionable
{
use Position;
use ShortClassNameProvider;
/**
* @param int $iLineNo
* @param int<1, max>|null $lineNumber
*/
public function __construct($iLineNo = 0)
public function __construct(?int $lineNumber = null)
{
$this->setPosition($iLineNo);
$this->setPosition($lineNumber);
}
/**
* @param array<array-key, string> $aListDelimiters
* @param array<non-empty-string> $listDelimiters
*
* @return RuleValueList|CSSFunction|CSSString|LineName|Size|URL|string
* @return Value|string
*
* @throws UnexpectedTokenException
* @throws UnexpectedEOFException
*
* @internal since V8.8.0
*/
public static function parseValue(ParserState $oParserState, array $aListDelimiters = [])
public static function parseValue(ParserState $parserState, array $listDelimiters = [])
{
/** @var array<int, RuleValueList|CSSFunction|CSSString|LineName|Size|URL|string> $aStack */
$aStack = [];
$oParserState->consumeWhiteSpace();
/** @var list<Value|string> $stack */
$stack = [];
$parserState->consumeWhiteSpace();
//Build a list of delimiters and parsed values
while (
!($oParserState->comes('}') || $oParserState->comes(';') || $oParserState->comes('!')
|| $oParserState->comes(')')
|| $oParserState->comes('\\')
|| $oParserState->isEnd())
!($parserState->comes('}') || $parserState->comes(';') || $parserState->comes('!')
|| $parserState->comes(')')
|| $parserState->isEnd())
) {
if (count($aStack) > 0) {
$bFoundDelimiter = false;
foreach ($aListDelimiters as $sDelimiter) {
if ($oParserState->comes($sDelimiter)) {
array_push($aStack, $oParserState->consume($sDelimiter));
$oParserState->consumeWhiteSpace();
$bFoundDelimiter = true;
if (\count($stack) > 0) {
$foundDelimiter = false;
foreach ($listDelimiters as $delimiter) {
if ($parserState->comes($delimiter)) {
\array_push($stack, $parserState->consume($delimiter));
$parserState->consumeWhiteSpace();
$foundDelimiter = true;
break;
}
}
if (!$bFoundDelimiter) {
if (!$foundDelimiter) {
//Whitespace was the list delimiter
array_push($aStack, ' ');
\array_push($stack, ' ');
}
}
array_push($aStack, self::parsePrimitiveValue($oParserState));
$oParserState->consumeWhiteSpace();
\array_push($stack, self::parsePrimitiveValue($parserState));
$parserState->consumeWhiteSpace();
}
// Convert the list to list objects
foreach ($aListDelimiters as $sDelimiter) {
$iStackLength = count($aStack);
if ($iStackLength === 1) {
return $aStack[0];
foreach ($listDelimiters as $delimiter) {
$stackSize = \count($stack);
if ($stackSize === 1) {
return $stack[0];
}
$aNewStack = [];
for ($iStartPosition = 0; $iStartPosition < $iStackLength; ++$iStartPosition) {
if ($iStartPosition === ($iStackLength - 1) || $sDelimiter !== $aStack[$iStartPosition + 1]) {
$aNewStack[] = $aStack[$iStartPosition];
$newStack = [];
for ($offset = 0; $offset < $stackSize; ++$offset) {
if ($offset === ($stackSize - 1) || $delimiter !== $stack[$offset + 1]) {
$newStack[] = $stack[$offset];
continue;
}
$iLength = 2; //Number of elements to be joined
for ($i = $iStartPosition + 3; $i < $iStackLength; $i += 2, ++$iLength) {
if ($sDelimiter !== $aStack[$i]) {
$length = 2; //Number of elements to be joined
for ($i = $offset + 3; $i < $stackSize; $i += 2, ++$length) {
if ($delimiter !== $stack[$i]) {
break;
}
}
$oList = new RuleValueList($sDelimiter, $oParserState->currentLine());
for ($i = $iStartPosition; $i - $iStartPosition < $iLength * 2; $i += 2) {
$oList->addListComponent($aStack[$i]);
$list = new RuleValueList($delimiter, $parserState->currentLine());
for ($i = $offset; $i - $offset < $length * 2; $i += 2) {
$list->addListComponent($stack[$i]);
}
$aNewStack[] = $oList;
$iStartPosition += $iLength * 2 - 2;
$newStack[] = $list;
$offset += $length * 2 - 2;
}
$aStack = $aNewStack;
$stack = $newStack;
}
if (!isset($aStack[0])) {
if (!isset($stack[0])) {
throw new UnexpectedTokenException(
" {$oParserState->peek()} ",
$oParserState->peek(1, -1) . $oParserState->peek(2),
" {$parserState->peek()} ",
$parserState->peek(1, -1) . $parserState->peek(2),
'literal',
$oParserState->currentLine()
$parserState->currentLine()
);
}
return $aStack[0];
return $stack[0];
}
/**
* @param bool $bIgnoreCase
*
* @return CSSFunction|string
*
* @throws UnexpectedEOFException
@@ -114,27 +117,23 @@ abstract class Value implements CSSElement, Positionable
*
* @internal since V8.8.0
*/
public static function parseIdentifierOrFunction(ParserState $oParserState, $bIgnoreCase = false)
public static function parseIdentifierOrFunction(ParserState $parserState, bool $ignoreCase = false)
{
$oAnchor = $oParserState->anchor();
$mResult = $oParserState->parseIdentifier($bIgnoreCase);
$anchor = $parserState->anchor();
$result = $parserState->parseIdentifier($ignoreCase);
if ($oParserState->comes('(')) {
$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);
if ($parserState->comes('(')) {
$anchor->backtrack();
if ($parserState->streql('url', $result)) {
$result = URL::parse($parserState);
} elseif ($parserState->streql('calc', $result)) {
$result = CalcFunction::parse($parserState);
} else {
$mResult = CSSFunction::parse($oParserState, $bIgnoreCase);
$result = CSSFunction::parse($parserState, $ignoreCase);
}
}
return $mResult;
return $result;
}
/**
@@ -146,73 +145,84 @@ abstract class Value implements CSSElement, Positionable
*
* @internal since V8.8.0
*/
public static function parsePrimitiveValue(ParserState $oParserState)
public static function parsePrimitiveValue(ParserState $parserState)
{
$oValue = null;
$oParserState->consumeWhiteSpace();
$parserState->consumeWhiteSpace();
if (
is_numeric($oParserState->peek())
|| ($oParserState->comes('-.')
&& is_numeric($oParserState->peek(1, 2)))
|| (($oParserState->comes('-') || $oParserState->comes('.')) && is_numeric($oParserState->peek(1, 1)))
\is_numeric($parserState->peek())
|| ($parserState->comes('-.')
&& \is_numeric($parserState->peek(1, 2)))
|| (($parserState->comes('-') || $parserState->comes('.')) && \is_numeric($parserState->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("'") || $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);
$value = Size::parse($parserState);
} elseif ($parserState->comes('#') || $parserState->comes('rgb', true) || $parserState->comes('hsl', true)) {
$value = Color::parse($parserState);
} elseif ($parserState->comes("'") || $parserState->comes('"')) {
$value = CSSString::parse($parserState);
} elseif ($parserState->comes('progid:') && $parserState->getSettings()->usesLenientParsing()) {
$value = self::parseMicrosoftFilter($parserState);
} elseif ($parserState->comes('[')) {
$value = LineName::parse($parserState);
} elseif ($parserState->comes('U+')) {
$value = self::parseUnicodeRangeValue($parserState);
} else {
$sNextChar = $oParserState->peek(1);
$nextCharacter = $parserState->peek(1);
try {
$oValue = self::parseIdentifierOrFunction($oParserState);
$value = self::parseIdentifierOrFunction($parserState);
} catch (UnexpectedTokenException $e) {
if (\in_array($sNextChar, ['+', '-', '*', '/'], true)) {
$oValue = $oParserState->consume(1);
if (\in_array($nextCharacter, ['+', '-', '*', '/'], true)) {
$value = $parserState->consume(1);
} else {
throw $e;
}
}
}
$oParserState->consumeWhiteSpace();
return $oValue;
$parserState->consumeWhiteSpace();
return $value;
}
/**
* @return CSSFunction
* @return array<string, bool|int|float|string|array<mixed>|null>
*
* @throws UnexpectedEOFException
* @throws UnexpectedTokenException
* @internal
*/
private static function parseMicrosoftFilter(ParserState $oParserState)
public function getArrayRepresentation(): array
{
$sFunction = $oParserState->consumeUntil('(', false, true);
$aArguments = Value::parseValue($oParserState, [',', '=']);
return new CSSFunction($sFunction, $aArguments, ',', $oParserState->currentLine());
return [
'class' => $this->getShortClassName(),
];
}
/**
* @return string
*
* @throws UnexpectedEOFException
* @throws UnexpectedTokenException
*/
private static function parseUnicodeRangeValue(ParserState $oParserState)
private static function parseMicrosoftFilter(ParserState $parserState): CSSFunction
{
$iCodepointMaxLength = 6; // Code points outside BMP can use up to six digits
$sRange = "";
$oParserState->consume("U+");
$function = $parserState->consumeUntil('(', false, true);
$arguments = Value::parseValue($parserState, [',', '=']);
return new CSSFunction($function, $arguments, ',', $parserState->currentLine());
}
/**
* @throws UnexpectedEOFException
* @throws UnexpectedTokenException
*/
private static function parseUnicodeRangeValue(ParserState $parserState): string
{
$codepointMaxLength = 6; // Code points outside BMP can use up to six digits
$range = '';
$parserState->consume('U+');
do {
if ($oParserState->comes('-')) {
$iCodepointMaxLength = 13; // Max length is 2 six digit code points + the dash(-) between them
if ($parserState->comes('-')) {
$codepointMaxLength = 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}";
$range .= $parserState->consume(1);
} while (
(\strlen($range) < $codepointMaxLength) && (preg_match('/[A-Fa-f0-9\\?-]/', $parserState->peek()) === 1)
);
return "U+{$range}";
}
}
+64 -54
View File
@@ -1,8 +1,11 @@
<?php
declare(strict_types=1);
namespace Sabberworm\CSS\Value;
use Sabberworm\CSS\OutputFormat;
use Sabberworm\CSS\ShortClassNameProvider;
/**
* A `ValueList` represents a lists of `Value`s, separated by some separation character
@@ -12,103 +15,110 @@ use Sabberworm\CSS\OutputFormat;
*/
abstract class ValueList extends Value
{
use ShortClassNameProvider;
/**
* @var array<int, RuleValueList|CSSFunction|CSSString|LineName|Size|URL|string>
* @var array<Value|string>
*
* @internal since 8.8.0
*/
protected $aComponents;
protected $components;
/**
* @var string
* @var non-empty-string
*
* @internal since 8.8.0
*/
protected $sSeparator;
protected $separator;
/**
* 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
* @param array<Value|string>|Value|string $components
* @param non-empty-string $separator
* @param int<1, max>|null $lineNumber
*/
public function __construct($aComponents = [], $sSeparator = ',', $iLineNo = 0)
public function __construct($components = [], $separator = ',', ?int $lineNumber = null)
{
parent::__construct($iLineNo);
if (!is_array($aComponents)) {
$aComponents = [$aComponents];
parent::__construct($lineNumber);
if (!\is_array($components)) {
$components = [$components];
}
$this->aComponents = $aComponents;
$this->sSeparator = $sSeparator;
$this->components = $components;
$this->separator = $separator;
}
/**
* @param RuleValueList|CSSFunction|CSSString|LineName|Size|URL|string $mComponent
*
* @return void
* @param Value|string $component
*/
public function addListComponent($mComponent)
public function addListComponent($component): void
{
$this->aComponents[] = $mComponent;
$this->components[] = $component;
}
/**
* @return array<int, RuleValueList|CSSFunction|CSSString|LineName|Size|URL|string>
* @return array<Value|string>
*/
public function getListComponents()
public function getListComponents(): array
{
return $this->aComponents;
return $this->components;
}
/**
* @param array<int, RuleValueList|CSSFunction|CSSString|LineName|Size|URL|string> $aComponents
*
* @return void
* @param array<Value|string> $components
*/
public function setListComponents(array $aComponents)
public function setListComponents(array $components): void
{
$this->aComponents = $aComponents;
$this->components = $components;
}
/**
* @return string
* @return non-empty-string
*/
public function getListSeparator()
public function getListSeparator(): string
{
return $this->sSeparator;
return $this->separator;
}
/**
* @param string $sSeparator
*
* @return void
* @param non-empty-string $separator
*/
public function setListSeparator($sSeparator)
public function setListSeparator(string $separator): void
{
$this->sSeparator = $sSeparator;
$this->separator = $separator;
}
/**
* @return string
*
* @deprecated in V8.8.0, will be removed in V9.0.0. Use `render` instead.
*/
public function __toString()
public function render(OutputFormat $outputFormat): string
{
return $this->render(new OutputFormat());
}
$formatter = $outputFormat->getFormatter();
/**
* @param OutputFormat|null $oOutputFormat
*
* @return string
*/
public function render($oOutputFormat)
{
return $oOutputFormat->implode(
$oOutputFormat->spaceBeforeListArgumentSeparator($this->sSeparator) . $this->sSeparator
. $oOutputFormat->spaceAfterListArgumentSeparator($this->sSeparator),
$this->aComponents
return $formatter->implode(
$formatter->spaceBeforeListArgumentSeparator($this->separator) . $this->separator
. $formatter->spaceAfterListArgumentSeparator($this->separator),
$this->components
);
}
/**
* @return array<string, bool|int|float|string|array<mixed>|null>
*
* @internal
*/
public function getArrayRepresentation(): array
{
return [
'class' => $this->getShortClassName(),
'components' => \array_map(
/**
* @parm Value|string $component
*/
function ($component): array {
if (\is_string($component)) {
return ['class' => 'string', 'value' => $component];
}
return $component->getArrayRepresentation();
},
$this->components
),
'separator' => $this->separator,
];
}
}