From 9fc40c64d9b9d28f3d8dd857889e2a14b1b0b39d Mon Sep 17 00:00:00 2001 From: fukayatsu Date: Thu, 7 May 2026 13:41:59 +0900 Subject: [PATCH] Accept CSS numbers without a leading zero in rgb/rgba/hsl/hsla The numeric colour regexps used `\d+(\.\d+)?` which requires at least one digit before the decimal point, so values like `rgba(0,0,0,.1)` were not recognised as colours. Per CSS Values & Units, `` permits the integer part to be omitted (`[0-9]* '.' [0-9]+`). The practical impact is that shorthand expansion silently dropped the colour: border: 1px solid rgba(0,0,0,.1) -> border-top-width / -style are kept, border-top-color is gone. background: url(x.png) rgba(0,0,0,.1) -> background-image is kept, background-color is gone. This bites users of Dart Sass + Premailer because Dart Sass's `compressed` output strips leading zeros, so `.1` is what the inliner actually receives. Switching the integer-and-optional-fraction pattern to `(?:\d*\.)?\d+` accepts `1`, `1.5`, and `.5` while still rejecting a bare `1.` (also invalid per spec). Tests cover both the regex itself (positive cases for `.1`-style values) and the shorthand-expansion regression for `border:` and `background:`. --- CHANGELOG.md | 2 ++ lib/css_parser/regexps.rb | 6 ++++-- test/test_css_parser_regexps.rb | 1 + test/test_rule_set_expanding_shorthand.rb | 18 +++++++++++++++++- 4 files changed, 24 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c05460..cb9ad2a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ### Unreleased +* Accept CSS `` values with an omitted integer part (e.g. `.1`) inside `rgb()`/`rgba()`/`hsl()`/`hsla()`. Previously `RE_COLOUR_NUMERIC` and `RE_COLOUR_NUMERIC_ALPHA` required at least one digit before the decimal point, which caused colours such as `rgba(0,0,0,.1)` to be silently dropped during shorthand expansion (`background-color` from `background:`, `border-*-color` from `border:`). + ### Version 2.1.0 * Validate ssl when pulling files via https diff --git a/lib/css_parser/regexps.rb b/lib/css_parser/regexps.rb index cf83b2d..911570b 100644 --- a/lib/css_parser/regexps.rb +++ b/lib/css_parser/regexps.rb @@ -259,8 +259,10 @@ def self.regex_possible_values(*values) inherit currentColor ].freeze - RE_COLOUR_NUMERIC = /\b(hsl|rgb)\s*\(-?\s*-?\d+(\.\d+)?%?\s*%?,-?\s*-?\d+(\.\d+)?%?\s*%?,-?\s*-?\d+(\.\d+)?%?\s*%?\)/i.freeze - RE_COLOUR_NUMERIC_ALPHA = /\b(hsla|rgba)\s*\(-?\s*-?\d+(\.\d+)?%?\s*%?,-?\s*-?\d+(\.\d+)?%?\s*%?,-?\s*-?\d+(\.\d+)?%?\s*%?,-?\s*-?\d+(\.\d+)?%?\s*%?\)/i.freeze + # CSS allows the integer part to be omitted (e.g. `.1`), per CSS Values & Units. + # `(?:\d*\.)?\d+` accepts `1`, `1.5`, and `.5` while still rejecting bare `1.`. + RE_COLOUR_NUMERIC = /\b(hsl|rgb)\s*\(-?\s*-?(?:\d*\.)?\d+%?\s*%?,-?\s*-?(?:\d*\.)?\d+%?\s*%?,-?\s*-?(?:\d*\.)?\d+%?\s*%?\)/i.freeze + RE_COLOUR_NUMERIC_ALPHA = /\b(hsla|rgba)\s*\(-?\s*-?(?:\d*\.)?\d+%?\s*%?,-?\s*-?(?:\d*\.)?\d+%?\s*%?,-?\s*-?(?:\d*\.)?\d+%?\s*%?,-?\s*-?(?:\d*\.)?\d+%?\s*%?\)/i.freeze RE_COLOUR_HEX = /\s*#([0-9a-fA-F]{6}|[0-9a-fA-F]{3})\b/.freeze RE_COLOUR_NAMED = /\s*\b(#{NAMED_COLOURS.join('|')})\b/i.freeze RE_COLOUR = Regexp.union(RE_COLOUR_NUMERIC, RE_COLOUR_NUMERIC_ALPHA, RE_COLOUR_HEX, RE_COLOUR_NAMED) diff --git a/test/test_css_parser_regexps.rb b/test/test_css_parser_regexps.rb index 6404017..1394d36 100644 --- a/test/test_css_parser_regexps.rb +++ b/test/test_css_parser_regexps.rb @@ -40,6 +40,7 @@ def test_colour 'color: #fff', 'color:#f0a09c;', 'color: #04A', 'color: #04a9CE', 'color: rgb(100, -10%, 300);', 'color: rgb(10,10,10)', 'color:rgb(12.7253%, -12%,0)', 'color: hsla(-15, -77%, 19%, 5%);', + 'color: rgba(0,0,0,.1)', 'color: rgba(0, 0, 0, .5)', 'color: hsla(0, 0%, 0%, .05)', 'color: black', 'color:Red;', 'color: AqUa;', 'color: blue ', 'color: transparent', 'color: darkslategray' ].each do |colour| diff --git a/test/test_rule_set_expanding_shorthand.rb b/test/test_rule_set_expanding_shorthand.rb index ea7b719..e0b7476 100644 --- a/test/test_rule_set_expanding_shorthand.rb +++ b/test/test_rule_set_expanding_shorthand.rb @@ -18,6 +18,17 @@ def test_expanding_border_shorthand assert_equal '1px', declarations['border-top-width'] assert_equal 'solid', declarations['border-bottom-style'] + # Regression: rgba/hsla with no leading zero on the alpha (e.g. `.1`) used + # to fail the colour regex, causing border-*-color to be silently dropped + # during shorthand expansion. + declarations = expand_declarations('border: 1px solid rgba(0,0,0,.1)') + assert_equal '1px', declarations['border-top-width'] + assert_equal 'solid', declarations['border-top-style'] + assert_equal 'rgba(0,0,0,.1)', declarations['border-top-color'] + assert_equal 'rgba(0,0,0,.1)', declarations['border-right-color'] + assert_equal 'rgba(0,0,0,.1)', declarations['border-bottom-color'] + assert_equal 'rgba(0,0,0,.1)', declarations['border-left-color'] + declarations = expand_declarations('border-color: red hsla(255, 0, 0, 5) rgb(2% ,2%,2%)') assert_equal 'red', declarations['border-top-color'] assert_equal 'rgb(2%,2%,2%)', declarations['border-bottom-color'] @@ -197,7 +208,12 @@ def test_getting_background_size_from_shorthand end def test_getting_background_colour_from_shorthand - ['blue', 'lime', 'rgb(10,10,10)', 'rgb ( -10%, 99, 300)', '#ffa0a0', '#03c', 'trAnsparEnt', 'inherit'].each do |colour| + [ + 'blue', 'lime', 'rgb(10,10,10)', 'rgb ( -10%, 99, 300)', '#ffa0a0', '#03c', 'trAnsparEnt', 'inherit', + # Regression: alpha without a leading zero (e.g. `.1`) used to fail the + # colour regex and silently drop background-color. + 'rgba(0,0,0,.1)' + ].each do |colour| shorthand = "background:#{colour} url('chess.png') center repeat fixed ;" declarations = expand_declarations(shorthand) assert_equal(colour, declarations['background-color'])