diff --git a/components/DataLiberation/CSS/class-cssprocessor.php b/components/DataLiberation/CSS/class-cssprocessor.php index e7f50adb..9b92a100 100644 --- a/components/DataLiberation/CSS/class-cssprocessor.php +++ b/components/DataLiberation/CSS/class-cssprocessor.php @@ -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, and have a type + * flag indicating whether the number was written as an integer or a number + * (with decimal point or exponent). 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. * @@ -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). * @@ -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; } @@ -1098,8 +1133,6 @@ 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 * @@ -1107,6 +1140,8 @@ private function consume_string(): 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. @@ -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 ) { @@ -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 ) { @@ -1183,14 +1222,16 @@ private function consume_numeric(): bool { // Consume an ident sequence. Set the '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 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; @@ -1199,8 +1240,9 @@ private function consume_numeric(): bool { } // Otherwise, create a 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; } diff --git a/components/DataLiberation/Tests/CSSProcessorTest.php b/components/DataLiberation/Tests/CSSProcessorTest.php index a6f7f7f3..e520b972 100644 --- a/components/DataLiberation/Tests/CSSProcessorTest.php +++ b/components/DataLiberation/Tests/CSSProcessorTest.php @@ -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 ) ); @@ -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, and have a type + * flag of "integer" or "number". 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. */ diff --git a/components/DataLiberation/Tests/css-test-cases.json b/components/DataLiberation/Tests/css-test-cases.json index c1ead99f..1677bdc5 100644 --- a/components/DataLiberation/Tests/css-test-cases.json +++ b/components/DataLiberation/Tests/css-test-cases.json @@ -58,7 +58,8 @@ "startIndex": 1, "endIndex": 3, "normalized": "-1", - "value": "-1" + "value": "-1", + "numberType": "integer" }, { "type": "whitespace-token", @@ -686,7 +687,8 @@ "startIndex": 0, "endIndex": 1, "normalized": "0", - "value": "0" + "value": "0", + "numberType": "integer" }, { "type": "whitespace-token", @@ -702,7 +704,8 @@ "startIndex": 2, "endIndex": 3, "normalized": "1", - "value": "1" + "value": "1", + "numberType": "integer" }, { "type": "whitespace-token", @@ -718,7 +721,8 @@ "startIndex": 4, "endIndex": 5, "normalized": "2", - "value": "2" + "value": "2", + "numberType": "integer" }, { "type": "whitespace-token", @@ -734,7 +738,8 @@ "startIndex": 6, "endIndex": 7, "normalized": "3", - "value": "3" + "value": "3", + "numberType": "integer" }, { "type": "whitespace-token", @@ -750,7 +755,8 @@ "startIndex": 8, "endIndex": 9, "normalized": "4", - "value": "4" + "value": "4", + "numberType": "integer" }, { "type": "whitespace-token", @@ -766,7 +772,8 @@ "startIndex": 10, "endIndex": 11, "normalized": "5", - "value": "5" + "value": "5", + "numberType": "integer" }, { "type": "whitespace-token", @@ -782,7 +789,8 @@ "startIndex": 12, "endIndex": 13, "normalized": "6", - "value": "6" + "value": "6", + "numberType": "integer" }, { "type": "whitespace-token", @@ -798,7 +806,8 @@ "startIndex": 14, "endIndex": 15, "normalized": "7", - "value": "7" + "value": "7", + "numberType": "integer" }, { "type": "whitespace-token", @@ -814,7 +823,8 @@ "startIndex": 16, "endIndex": 17, "normalized": "8", - "value": "8" + "value": "8", + "numberType": "integer" }, { "type": "whitespace-token", @@ -830,7 +840,8 @@ "startIndex": 18, "endIndex": 19, "normalized": "9", - "value": "9" + "value": "9", + "numberType": "integer" }, { "type": "whitespace-token", @@ -852,7 +863,8 @@ "endIndex": 4, "normalized": "10px", "value": "10", - "unit": "px" + "unit": "px", + "numberType": "integer" }, { "type": "whitespace-token", @@ -874,7 +886,8 @@ "endIndex": 7, "normalized": "10px", "value": "10", - "unit": "px" + "unit": "px", + "numberType": "integer" }, { "type": "whitespace-token", @@ -896,7 +909,8 @@ "endIndex": 13, "normalized": "10--custom-px", "value": "10", - "unit": "--custom-px" + "unit": "--custom-px", + "numberType": "integer" }, { "type": "whitespace-token", @@ -918,7 +932,8 @@ "endIndex": 6, "normalized": "10e2px", "value": "10e2", - "unit": "px" + "unit": "px", + "numberType": "number" }, { "type": "whitespace-token", @@ -940,7 +955,8 @@ "endIndex": 6, "normalized": "10E2PX", "value": "10E2", - "unit": "PX" + "unit": "PX", + "numberType": "number" }, { "type": "whitespace-token", @@ -962,7 +978,8 @@ "endIndex": 5, "normalized": "10�", "value": "10", - "unit": "�" + "unit": "�", + "numberType": "integer" } ] }, @@ -976,7 +993,8 @@ "endIndex": 7, "normalized": "10a𐀀", "value": "10", - "unit": "a𐀀" + "unit": "a𐀀", + "numberType": "integer" }, { "type": "whitespace-token", @@ -998,7 +1016,8 @@ "endIndex": 4, "normalized": "10a�", "value": "10", - "unit": "a�" + "unit": "a�", + "numberType": "integer" } ] }, @@ -1389,7 +1408,8 @@ "startIndex": 0, "endIndex": 2, "normalized": ".1", - "value": ".1" + "value": ".1", + "numberType": "number" }, { "type": "whitespace-token", @@ -1411,7 +1431,8 @@ "endIndex": 28, "normalized": "4waPtwEEGH�jV3zM6hh6w30N0PC", "value": "4", - "unit": "waPtwEEGH�jV3zM6hh6w30N0PC" + "unit": "waPtwEEGH�jV3zM6hh6w30N0PC", + "numberType": "integer" }, { "type": "whitespace-token", @@ -1428,7 +1449,8 @@ "endIndex": 46, "normalized": "7m8KM0HcWGOPw28Gt", "value": "7", - "unit": "m8KM0HcWGOPw28Gt" + "unit": "m8KM0HcWGOPw28Gt", + "numberType": "integer" }, { "type": "(-token", @@ -1490,7 +1512,8 @@ "endIndex": 11, "normalized": "808G", "value": "808", - "unit": "G" + "unit": "G", + "numberType": "integer" }, { "type": "string-token", @@ -1552,7 +1575,8 @@ "endIndex": 29, "normalized": "7rSD6I5L1lglVRlL2X7BbEk\u0003HCd", "value": "7", - "unit": "rSD6I5L1lglVRlL2X7BbEk\u0003HCd" + "unit": "rSD6I5L1lglVRlL2X7BbEk\u0003HCd", + "numberType": "integer" }, { "type": "whitespace-token", @@ -1568,7 +1592,8 @@ "startIndex": 30, "endIndex": 32, "normalized": "94", - "value": "94" + "value": "94", + "numberType": "integer" }, { "type": "whitespace-token", @@ -1714,7 +1739,8 @@ "startIndex": 0, "endIndex": 3, "normalized": "565", - "value": "565" + "value": "565", + "numberType": "integer" }, { "type": "string-token", @@ -1792,7 +1818,8 @@ "endIndex": 29, "normalized": "8lU0j6B186kh", "value": "8", - "unit": "lU0j6B186kh" + "unit": "lU0j6B186kh", + "numberType": "integer" }, { "type": "whitespace-token", @@ -1870,7 +1897,8 @@ "endIndex": 28, "normalized": "0B120h5QUbNbmTD2K8mAD傿i", "value": "0", - "unit": "B120h5QUbNbmTD2K8mAD傿i" + "unit": "B120h5QUbNbmTD2K8mAD傿i", + "numberType": "integer" }, { "type": "delim-token", @@ -1948,7 +1976,8 @@ "endIndex": 164, "normalized": "5528LZ14", "value": "5528", - "unit": "LZ14" + "unit": "LZ14", + "numberType": "integer" }, { "type": ")-token", @@ -2049,7 +2078,8 @@ "startIndex": 14, "endIndex": 15, "normalized": "9", - "value": "9" + "value": "9", + "numberType": "integer" }, { "type": "comma-token", @@ -2095,7 +2125,8 @@ "endIndex": 11, "normalized": "1E", "value": "1", - "unit": "E" + "unit": "E", + "numberType": "integer" }, { "type": "delim-token", @@ -2157,7 +2188,8 @@ "endIndex": 10, "normalized": "7Zkc0P17", "value": "7", - "unit": "Zkc0P17" + "unit": "Zkc0P17", + "numberType": "integer" }, { "type": "whitespace-token", @@ -2181,7 +2213,8 @@ "startIndex": 27, "endIndex": 29, "normalized": ".2", - "value": ".2" + "value": ".2", + "numberType": "number" }, { "type": "number-token", @@ -2189,7 +2222,8 @@ "startIndex": 29, "endIndex": 31, "normalized": ".7", - "value": ".7" + "value": ".7", + "numberType": "number" }, { "type": "comma-token", @@ -2623,7 +2657,8 @@ "startIndex": 0, "endIndex": 2, "normalized": "-1", - "value": "-1" + "value": "-1", + "numberType": "integer" }, { "type": "whitespace-token", @@ -2644,7 +2679,8 @@ "startIndex": 0, "endIndex": 3, "normalized": "-.1", - "value": "-.1" + "value": "-.1", + "numberType": "number" }, { "type": "whitespace-token", @@ -2686,7 +2722,8 @@ "startIndex": 0, "endIndex": 2, "normalized": "-0", - "value": "-0" + "value": "-0", + "numberType": "integer" }, { "type": "whitespace-token", @@ -3397,7 +3434,8 @@ "startIndex": 0, "endIndex": 2, "normalized": "10", - "value": "10" + "value": "10", + "numberType": "integer" }, { "type": "whitespace-token", @@ -3418,7 +3456,8 @@ "startIndex": 0, "endIndex": 3, "normalized": "+10", - "value": "+10" + "value": "+10", + "numberType": "integer" }, { "type": "whitespace-token", @@ -3439,7 +3478,8 @@ "startIndex": 0, "endIndex": 3, "normalized": "-10", - "value": "-10" + "value": "-10", + "numberType": "integer" }, { "type": "whitespace-token", @@ -3460,7 +3500,8 @@ "startIndex": 0, "endIndex": 1, "normalized": "0", - "value": "0" + "value": "0", + "numberType": "integer" }, { "type": "whitespace-token", @@ -3481,7 +3522,8 @@ "startIndex": 0, "endIndex": 2, "normalized": "+0", - "value": "+0" + "value": "+0", + "numberType": "integer" }, { "type": "whitespace-token", @@ -3502,7 +3544,8 @@ "startIndex": 0, "endIndex": 2, "normalized": "-0", - "value": "-0" + "value": "-0", + "numberType": "integer" }, { "type": "whitespace-token", @@ -3523,7 +3566,8 @@ "startIndex": 0, "endIndex": 2, "normalized": ".0", - "value": ".0" + "value": ".0", + "numberType": "number" }, { "type": "whitespace-token", @@ -3544,7 +3588,8 @@ "startIndex": 0, "endIndex": 2, "normalized": ".1", - "value": ".1" + "value": ".1", + "numberType": "number" }, { "type": "whitespace-token", @@ -3565,7 +3610,8 @@ "startIndex": 0, "endIndex": 3, "normalized": "+.1", - "value": "+.1" + "value": "+.1", + "numberType": "number" }, { "type": "whitespace-token", @@ -3586,7 +3632,8 @@ "startIndex": 0, "endIndex": 3, "normalized": "-.1", - "value": "-.1" + "value": "-.1", + "numberType": "number" }, { "type": "whitespace-token", @@ -3607,7 +3654,8 @@ "startIndex": 0, "endIndex": 3, "normalized": "1.1", - "value": "1.1" + "value": "1.1", + "numberType": "number" }, { "type": "whitespace-token", @@ -3628,7 +3676,8 @@ "startIndex": 0, "endIndex": 4, "normalized": "+1.1", - "value": "+1.1" + "value": "+1.1", + "numberType": "number" }, { "type": "whitespace-token", @@ -3649,7 +3698,8 @@ "startIndex": 0, "endIndex": 4, "normalized": "-1.1", - "value": "-1.1" + "value": "-1.1", + "numberType": "number" }, { "type": "whitespace-token", @@ -3670,7 +3720,8 @@ "startIndex": 0, "endIndex": 5, "normalized": "1.1e2", - "value": "1.1e2" + "value": "1.1e2", + "numberType": "number" }, { "type": "whitespace-token", @@ -3691,7 +3742,8 @@ "startIndex": 0, "endIndex": 7, "normalized": "+1.1e+2", - "value": "+1.1e+2" + "value": "+1.1e+2", + "numberType": "number" }, { "type": "whitespace-token", @@ -3712,7 +3764,8 @@ "startIndex": 0, "endIndex": 7, "normalized": "-1.1e-2", - "value": "-1.1e-2" + "value": "-1.1e-2", + "numberType": "number" }, { "type": "whitespace-token", @@ -3733,7 +3786,8 @@ "startIndex": 0, "endIndex": 8, "normalized": "-1.1e-22", - "value": "-1.1e-22" + "value": "-1.1e-22", + "numberType": "number" }, { "type": "whitespace-token", @@ -3755,7 +3809,8 @@ "endIndex": 9, "normalized": "-1.1e-22e", "value": "-1.1e-22", - "unit": "e" + "unit": "e", + "numberType": "number" }, { "type": "whitespace-token", @@ -3777,7 +3832,8 @@ "endIndex": 2, "normalized": "1e", "value": "1", - "unit": "e" + "unit": "e", + "numberType": "integer" }, { "type": "delim-token", @@ -3806,7 +3862,8 @@ "startIndex": 0, "endIndex": 2, "normalized": ".2", - "value": ".2" + "value": ".2", + "numberType": "number" }, { "type": "number-token", @@ -3814,7 +3871,8 @@ "startIndex": 2, "endIndex": 4, "normalized": ".7", - "value": ".7" + "value": ".7", + "numberType": "number" }, { "type": "whitespace-token", @@ -3835,7 +3893,8 @@ "startIndex": 0, "endIndex": 11, "normalized": "-123.753e-2", - "value": "-123.753e-2" + "value": "-123.753e-2", + "numberType": "number" }, { "type": "whitespace-token", @@ -3857,7 +3916,8 @@ "endIndex": 13, "normalized": "-123.753e-2px", "value": "-123.753e-2", - "unit": "px" + "unit": "px", + "numberType": "number" }, { "type": "whitespace-token", @@ -3899,7 +3959,8 @@ "startIndex": 0, "endIndex": 3, "normalized": "1.2", - "value": "1.2" + "value": "1.2", + "numberType": "number" }, { "type": "number-token", @@ -3907,7 +3968,8 @@ "startIndex": 3, "endIndex": 5, "normalized": ".3", - "value": ".3" + "value": ".3", + "numberType": "number" }, { "type": "whitespace-token", @@ -3949,7 +4011,8 @@ "startIndex": 0, "endIndex": 2, "normalized": "+1", - "value": "+1" + "value": "+1", + "numberType": "integer" }, { "type": "whitespace-token", @@ -3970,7 +4033,8 @@ "startIndex": 0, "endIndex": 3, "normalized": "+.1", - "value": "+.1" + "value": "+.1", + "numberType": "number" }, { "type": "whitespace-token", @@ -3999,7 +4063,8 @@ "startIndex": 1, "endIndex": 3, "normalized": "+1", - "value": "+1" + "value": "+1", + "numberType": "integer" }, { "type": "whitespace-token",