Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
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
56 changes: 49 additions & 7 deletions components/DataLiberation/CSS/class-cssprocessor.php
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,20 @@ class CSSProcessor {
*/
private $token_unit = null;

/**
* The numeric type flag for the current token: "integer" or "number".
*
* Per CSS Syntax Level 3, <number-token> and <dimension-token> have a type
* flag indicating whether the number was written as an integer or a number
* (with decimal point or exponent). <percentage-token> does not have a type flag.
*
* @see https://www.w3.org/TR/css-syntax-3/#consume-number
*
* @var string|null
* @phpstan-var 'integer'|'number'|null
*/
private $token_number_type = null;

/**
* Lexical replacements to apply to input CSS document.
*
Expand Down Expand Up @@ -793,6 +807,26 @@ public function get_token_unit(): ?string {
return $this->token_unit;
}

/**
* Gets the numeric type flag for number and dimension tokens.
*
* This flag is only set on number and dimension tokens. For
* percentage and other token types, this is always `null`.
*
* Returns "integer" when the number was written without a decimal point or
* exponent (e.g. "42", "+7"), and "number" when it was written with one
* (e.g. "42.0", "1e2", ".5"). Returns null for percentage tokens (which
* have no type flag per spec) and all non-numeric tokens.
*
* @see https://www.w3.org/TR/css-syntax-3/#consume-number
*
* @return string|null "integer", "number", or null.
* @phpstan-return 'integer'|'number'|null
*/
public function get_token_number_type(): ?string {
return $this->token_number_type;
}

/**
* Gets the byte at where the token value starts (for STRING and URL tokens).
*
Expand Down Expand Up @@ -979,6 +1013,7 @@ private function after_token(): void {
$this->token_length = null;
$this->token_value = null;
$this->token_unit = null;
$this->token_number_type = null;
$this->token_value_starts_at = null;
$this->token_value_length = null;
}
Expand Down Expand Up @@ -1098,15 +1133,15 @@ private function consume_string(): bool {
* Numbers can be integers or decimals, with optional sign and exponent.
* They can be followed by % (percentage) or an identifier (dimension).
*
* @TODO: Keep track of the "type" flag ("integer" or "number").
*
* @see https://www.w3.org/TR/css-syntax-3/#consume-numeric-token
* @see https://www.w3.org/TR/css-syntax-3/#consume-number
*
* @return bool
*/
private function consume_numeric(): bool {
// Consume a number and let number be the result.
// The type flag defaults to "integer".
$number_type = 'integer';

// If the next input code point is U+002B PLUS SIGN (+) or U+002D HYPHEN-MINUS (-),
// consume it and append it to repr.
Expand All @@ -1129,6 +1164,8 @@ private function consume_numeric(): bool {
) {
// Consume them.
++$this->at;
// Set type to "number".
$number_type = 'number';
// While the next input code point is a digit, consume it and append it to repr.
$digits = strspn( $this->css, '0123456789', $this->at );
if ( $digits > 0 ) {
Expand Down Expand Up @@ -1159,6 +1196,8 @@ private function consume_numeric(): bool {
}

if ( $has_exp ) {
// Set type to "number".
$number_type = 'number';
// While the next input code point is a digit, consume it and append it to repr.
$digits = strspn( $this->css, '0123456789', $this->at );
if ( $digits > 0 ) {
Expand All @@ -1183,14 +1222,16 @@ private function consume_numeric(): bool {
// Consume an ident sequence. Set the <dimension-token>'s unit to the returned value.
$unit_starts_at = $this->at;
$this->consume_ident_sequence();
$this->token_unit = $this->decode_string_or_url( $unit_starts_at, $this->at - $unit_starts_at );
$this->token_type = self::TOKEN_DIMENSION;
$this->token_length = $this->at - $this->token_starts_at;
$this->token_unit = $this->decode_string_or_url( $unit_starts_at, $this->at - $unit_starts_at );
$this->token_type = self::TOKEN_DIMENSION;
$this->token_number_type = $number_type;
$this->token_length = $this->at - $this->token_starts_at;
return true;
}

// Otherwise, if the next input code point is U+0025 PERCENTAGE SIGN (%), consume it.
// Create a <percentage-token> with the same value as number, and return it.
// Note: percentage tokens do not have a type flag per spec.
if ( $this->at < $this->length && '%' === $this->css[ $this->at ] ) {
++$this->at;
$this->token_type = self::TOKEN_PERCENTAGE;
Expand All @@ -1199,8 +1240,9 @@ private function consume_numeric(): bool {
}

// Otherwise, create a <number-token> with the same value and type flag as number, and return it.
$this->token_type = self::TOKEN_NUMBER;
$this->token_length = $this->at - $this->token_starts_at;
$this->token_type = self::TOKEN_NUMBER;
$this->token_number_type = $number_type;
$this->token_length = $this->at - $this->token_starts_at;
return true;
}

Expand Down
40 changes: 40 additions & 0 deletions components/DataLiberation/Tests/CSSProcessorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ static public function collect_tokens( CSSProcessor $processor, $keys = null ):
if ( null !== $processor->get_token_unit() ) {
$token['unit'] = $processor->get_token_unit();
}
if ( null !== $processor->get_token_number_type() ) {
$token['numberType'] = $processor->get_token_number_type();
}

if ( null !== $keys ) {
$token = array_intersect_key( $token, array_flip( $keys ) );
Expand Down Expand Up @@ -954,6 +957,43 @@ public function test_dimension_token_value(): void {
$this->assertSame( $expected, $actual_tokens );
}

/**
* Tests the numeric type flag for number, dimension, and percentage tokens.
*
* Per CSS Syntax Level 3, <number-token> and <dimension-token> have a type
* flag of "integer" or "number". <percentage-token> does not have a type flag.
*
* @dataProvider data_token_number_type
*/
public function test_token_number_type( string $css, ?string $expected_type ): void {
$processor = CSSProcessor::create( $css );
$this->assertTrue( $processor->next_token() );
$this->assertSame( $expected_type, $processor->get_token_number_type() );
}

public static function data_token_number_type(): array {
return array(
'integer' => array( '42', 'integer' ),
'positive integer' => array( '+42', 'integer' ),
'negative integer' => array( '-42', 'integer' ),
'zero' => array( '0', 'integer' ),
'decimal' => array( '42.0', 'number' ),
'decimal with fraction' => array( '42.5', 'number' ),
'leading decimal point' => array( '.5', 'number' ),
'exponent lowercase' => array( '1e2', 'number' ),
'exponent uppercase' => array( '1E2', 'number' ),
'exponent with plus' => array( '1E+2', 'number' ),
'exponent with minus' => array( '1e-2', 'number' ),
'dimension integer' => array( '10px', 'integer' ),
'dimension decimal' => array( '10.5px', 'number' ),
'dimension exponent' => array( '1e2px', 'number' ),
'percentage integer' => array( '20%', null ),
'percentage decimal' => array( '20.0%', null ),
'ident token' => array( 'red', null ),
'string token' => array( '"hello"', null ),
);
}

/**
* Tests that create() validates encoding and only accepts UTF-8.
*/
Expand Down
Loading