Fun way to spend an evening!
Code
Here it is:
/^rgba?\(\s*(?!\d+(?:\.|\s*\-?)\d+\.\d+)\-?(?:\d*\.\d+|\d+)(%?)(?:(?:\s*,\s*\-?(?:\d+|\d*\.\d+)\1){2}(?:\s*,\s*\-?(?:\d+|\d*\.\d+)%?)?|(?:(?:\s*\-?\d*\.\d+|\s*\-\d+|\s+\d+){2}|(?:\s*\-?(?:\d+|\d*\.\d+)%){2})(?:\s*\/\s*\-?(?:\d+|\d*\.\d+)%?)?)\s*\)$/i
This regex isn't case-insensitive for rgba/RGBA so we should probably keep the i
flag when running.
192 characters!
To avoid negative lookaheads and to only roughly match more common inputs, try
/^rgba?\(\d+(?:(?:\s*,\s*\d+){2}(?:\s*,\s*(?:\d*\.\d+|\d+)%?)?)|(?:(?:\s+\d+){2}(?:\s*\/\s*(?:\d*\.\d+|\d+)%?)?)\)$/
Note that this is only presently useful for testing validity. Adding reliable capture groups would lengthen and complicate it past a level I'm comfortable hand-rolling. We can extract the numbers after validating with something like:
regexStr
.match(/\((.+)\)/)[1]
.trim()
.split(/\s*[,\/]\s*|\s+/)
Background
The purpose of this code is particularly to match the current CSS allowed rgb/rgba values. Might not fit everyone's use case, but that's what the Bounty was for.
Since posting, CSS now allows rgb(R G B / A)
. It also allows percentages, negatives, and decimals for all values. These are therefore valid:
✔ rgb(1, -2, .3, -.2)
✔ rgb(1 -2 .3 / -.2)
✔ rgb(1 -2 .3 / -.2%)
✔ rgb(1% -2% .3% / -.2%)
✔ rgb(1% -2% .3% / -.2)
When using percentages, all three color values must be percentages as well. The alpha can be a percentage in any environment.
While writing this, I also found an area where implementing this was quite difficult with regex.
✔ rgb(1 .2.3)
✔ rgb(1-.2.3)
✘ rgb(1.2.3)
✘ rgb(1 -2.3)
The bottom 2 examples are false when using CSS.supports(color, str)
.
Essentially, if it looks like it's possible that the rgb()
only contains 2 values, it will register as invalid.
We can just handle this directly as a special case by creating a variable-length negative lookahead. This may be important to realize if we want to transfer this regex to another engine.
(?!\d+(?:\.|\s*\-?)\d+\.\d+)
It just rejects early on matches for 1.2.3
, 1 2.3
, and 1 -2.3
.
Code Walkthrough
This is a massive one, so I'll take it apart, piece-by-piece. I'm going to pretend we're dealing with Extended Mode and so I'll litter the regex with whitespace and comments to make things clearer.
^rgba?\(\s*
(?!\d+(?:\.|\s*\-?)\d+\.\d+) # negative lookahead
\-?(?:\d*\.\d+|\d+) # handle 1,-1,-.1,-1.1
(%?) # save optional percentage
- To start, we make the
a
optional and allow whitespace after the parentheses.
- We add our negative lookahead for the 2 special cases.
- We then match our first number. Our number can be an integer, fractional, and possibly negative.
- We capture the optional percentage. This is to save on characters later by taking advantage of backreferences. We save about 60 characters with this.
(?: # next 2-3 numbers either
(?: # 2 separated by commas
\s*,\s*
\-?(?:\d+|\d*\.\d+)
\1
){2}
(?: # optional third maybe-percentage
\s*,\s*
\-?(?:\d+|\d*\.\d+)
%?
)?
- Next, capture comma separated values.
\1
refers to the %
if it existed earlier
- Allow our third number to have a percentage irrespective of whether the previous numbers hade one.
|(?: # match space-separated numbers
(?: # non-percentages
\s*\-?\d*\.\d+ # space-optional decimal
|\s*\-\d+ # space-opt negative
|\s+\d+ # space-req integer
){2}
|(?: # or percentages
\s*
\-?(?:\d+|\d*\.\d+)
%
){2}
)
(?: # optional third maybe-percentage
\s*\/\s*
\-?(?:\d+|\d*\.\d+)
%?
)?
- First try matching non percentage numbers separated by either
.
, -
, or whitespace.
- If no match, try percentage numbers, space optional
- Optionally match the alpha value, separated by
/