@@ -49,7 +49,6 @@ const integerPattern = '[-+]?\\d+';
4949const numberPattern = `((${ integerPattern } )(\\.\\d+)?|[-+]?(\\.\\d+))(e[-+]?${ integerPattern } )?` ;
5050const percentPattern = `(${ numberPattern } )(%)` ;
5151const identRegEx = new RegExp ( `^${ identPattern } $` , 'i' ) ;
52- const integerRegEx = new RegExp ( `^${ integerPattern } $` ) ;
5352const numberRegEx = new RegExp ( `^${ numberPattern } $` ) ;
5453const percentRegEx = new RegExp ( `^${ percentPattern } $` ) ;
5554const stringRegEx = / ^ ( " [ ^ " ] * " | ' [ ^ ' ] * ' ) $ / ;
@@ -64,10 +63,9 @@ const anglePattern = `(${numberPattern})(deg|grad|rad|turn)`;
6463const lengthPattern = `(${ numberPattern } )(ch|cm|r?em|ex|in|lh|mm|pc|pt|px|q|vh|vmin|vmax|vw)` ;
6564const angleRegEx = new RegExp ( `^${ anglePattern } $` , 'i' ) ;
6665const calcRegEx = / ^ c a l c \( \s * ( .+ ) \s * \) $ / i;
67- const colorRegEx1 = / ^ # ( [ 0 - 9 a - f A - F ] { 3 , 4 } ) { 1 , 2 } $ / ;
68- const colorRegEx2 = / ^ r g b \( ( [ ^ ) ] * ) \) $ / ;
69- const colorRegEx3 = / ^ r g b a \( ( [ ^ ) ] * ) \) $ / ;
70- const colorRegEx4 = / ^ h s l a ? \( \s * ( - ? \d + | - ? \d * .\d + ) \s * , \s * ( - ? \d + | - ? \d * .\d + ) % \s * , \s * ( - ? \d + | - ? \d * .\d + ) % \s * ( , \s * ( - ? \d + | - ? \d * .\d + ) \s * ) ? \) / ;
66+ const colorHexRegEx = / ^ # ( [ 0 - 9 a - f ] { 3 , 4 } ) { 1 , 2 } $ / i;
67+ const colorFnSeparators = [ ',' , '/' , ' ' ] ;
68+ const colorFnRegex = / ^ ( h s l | r g b ) a ? \( \s * ( .+ ) \s * \) $ / i;
7169const lengthRegEx = new RegExp ( `^${ lengthPattern } $` , 'i' ) ;
7270const numericRegEx = new RegExp ( `^(${ numberPattern } )(%|${ identPattern } )?$` , 'i' ) ;
7371const timeRegEx = new RegExp ( `^(${ numberPattern } )(m?s)$` , 'i' ) ;
@@ -220,6 +218,41 @@ exports.parseMeasurement = function parseMeasurement(val) {
220218 return exports . parsePercent ( val ) ;
221219} ;
222220
221+ /**
222+ * https://drafts.csswg.org/cssom/#ref-for-alphavalue-def
223+ * https://drafts.csswg.org/cssom/#ref-for-alphavalue-def
224+ *
225+ * Browsers store a gradient alpha value as an 8 bit unsigned integer value when
226+ * given as a percentage, while they store a gradient alpha value as a decimal
227+ * value when given as a number, or when given an opacity value as a number or
228+ * percentage.
229+ */
230+ exports . parseAlpha = function parseAlpha ( val , is8Bit = false ) {
231+ if ( val === '' ) {
232+ return val ;
233+ }
234+ let parsed = exports . parseNumber ( val ) ;
235+ if ( parsed !== undefined ) {
236+ is8Bit = false ;
237+ val = Math . min ( 1 , Math . max ( 0 , parsed ) ) * 100 ;
238+ } else if ( ( parsed = exports . parsePercent ( val , true ) ) ) {
239+ val = Math . min ( 100 , Math . max ( 0 , parsed . slice ( 0 , - 1 ) ) ) ;
240+ } else {
241+ return undefined ;
242+ }
243+
244+ if ( ! is8Bit ) {
245+ return serializeNumber ( val / 100 ) ;
246+ }
247+
248+ // Fix JS precision (eg. 50 * 2.55 === 127.499... instead of 127.5) with toPrecision(15)
249+ const alpha = Math . round ( ( val * 2.55 ) . toPrecision ( 15 ) ) ;
250+ const integer = Math . round ( alpha / 2.55 ) ;
251+ const hasInteger = Math . round ( ( integer * 2.55 ) . toPrecision ( 15 ) ) === alpha ;
252+
253+ return String ( hasInteger ? integer / 100 : Math . round ( alpha / 0.255 ) / 1000 ) ;
254+ } ;
255+
223256/**
224257 * https://drafts.csswg.org/css-values-4/#angles
225258 * https://drafts.csswg.org/cssom/#ref-for-angle-value
@@ -526,115 +559,195 @@ exports.parseColor = function parseColor(val) {
526559 if ( val === '' ) {
527560 return val ;
528561 }
529- var red ,
530- green ,
531- blue ,
532- hue ,
533- saturation ,
534- lightness ,
535- alpha = 1 ;
536- var parts ;
537- var res = colorRegEx1 . exec ( val ) ;
538- // is it #aaa, #ababab, #aaaa, #abababaa
539- if ( res ) {
540- var defaultHex = val . substr ( 1 ) ;
541- var hex = val . substr ( 1 ) ;
542- if ( hex . length === 3 || hex . length === 4 ) {
543- hex = hex [ 0 ] + hex [ 0 ] + hex [ 1 ] + hex [ 1 ] + hex [ 2 ] + hex [ 2 ] ;
544-
545- if ( defaultHex . length === 4 ) {
546- hex = hex + defaultHex [ 3 ] + defaultHex [ 3 ] ;
547- }
548- }
549- red = parseInt ( hex . substr ( 0 , 2 ) , 16 ) ;
550- green = parseInt ( hex . substr ( 2 , 2 ) , 16 ) ;
551- blue = parseInt ( hex . substr ( 4 , 2 ) , 16 ) ;
552- if ( hex . length === 8 ) {
553- var hexAlpha = hex . substr ( 6 , 2 ) ;
554- var hexAlphaToRgbaAlpha = Number ( ( parseInt ( hexAlpha , 16 ) / 255 ) . toFixed ( 3 ) ) ;
555562
556- return 'rgba(' + red + ', ' + green + ', ' + blue + ', ' + hexAlphaToRgbaAlpha + ')' ;
557- }
558- return 'rgb(' + red + ', ' + green + ', ' + blue + ')' ;
559- }
563+ const rgb = [ ] ;
560564
561- res = colorRegEx2 . exec ( val ) ;
562- if ( res ) {
563- parts = res [ 1 ] . split ( / \s * , \s * / ) ;
564- if ( parts . length !== 3 ) {
565- return undefined ;
565+ /**
566+ * <hex-color>
567+ * value should be `#` followed by 3, 4, 6, or 8 hexadecimal digits
568+ * value should be resolved to <rgb()> | <rgba()>
569+ * value should be resolved to <rgb()> if <alpha> === 1
570+ */
571+ const hex = colorHexRegEx . exec ( val ) ;
572+
573+ if ( hex ) {
574+ const [ , n1 , n2 , n3 , n4 , n5 , n6 , n7 , n8 ] = val ;
575+ let alpha = 1 ;
576+
577+ switch ( val . length ) {
578+ case 4 :
579+ rgb . push ( Number ( `0x${ n1 } ${ n1 } ` ) , Number ( `0x${ n2 } ${ n2 } ` ) , Number ( `0x${ n3 } ${ n3 } ` ) ) ;
580+ break ;
581+ case 5 :
582+ rgb . push ( Number ( `0x${ n1 } ${ n1 } ` ) , Number ( `0x${ n2 } ${ n2 } ` ) , Number ( `0x${ n3 } ${ n3 } ` ) ) ;
583+ alpha = Number ( `0x${ n4 } ${ n4 } ` / 255 ) ;
584+ break ;
585+ case 7 :
586+ rgb . push ( Number ( `0x${ n1 } ${ n2 } ` ) , Number ( `0x${ n3 } ${ n4 } ` ) , Number ( `0x${ n5 } ${ n6 } ` ) ) ;
587+ break ;
588+ case 9 :
589+ rgb . push ( Number ( `0x${ n1 } ${ n2 } ` ) , Number ( `0x${ n3 } ${ n4 } ` ) , Number ( `0x${ n5 } ${ n6 } ` ) ) ;
590+ alpha = Number ( `0x${ n7 } ${ n8 } ` / 255 ) ;
591+ break ;
592+ default :
593+ return undefined ;
566594 }
567- if ( parts . every ( percentRegEx . test . bind ( percentRegEx ) ) ) {
568- red = Math . floor ( ( parseFloat ( parts [ 0 ] . slice ( 0 , - 1 ) ) * 255 ) / 100 ) ;
569- green = Math . floor ( ( parseFloat ( parts [ 1 ] . slice ( 0 , - 1 ) ) * 255 ) / 100 ) ;
570- blue = Math . floor ( ( parseFloat ( parts [ 2 ] . slice ( 0 , - 1 ) ) * 255 ) / 100 ) ;
571- } else if ( parts . every ( integerRegEx . test . bind ( integerRegEx ) ) ) {
572- red = parseInt ( parts [ 0 ] , 10 ) ;
573- green = parseInt ( parts [ 1 ] , 10 ) ;
574- blue = parseInt ( parts [ 2 ] , 10 ) ;
575- } else {
576- return undefined ;
595+
596+ if ( alpha == 1 ) {
597+ return `rgb(${ rgb . join ( ', ' ) } )` ;
577598 }
578- red = Math . min ( 255 , Math . max ( 0 , red ) ) ;
579- green = Math . min ( 255 , Math . max ( 0 , green ) ) ;
580- blue = Math . min ( 255 , Math . max ( 0 , blue ) ) ;
581- return 'rgb(' + red + ', ' + green + ', ' + blue + ')' ;
599+ return `rgba(${ rgb . join ( ', ' ) } , ${ + alpha . toFixed ( 3 ) } )` ;
582600 }
583601
584- res = colorRegEx3 . exec ( val ) ;
585- if ( res ) {
586- parts = res [ 1 ] . split ( / \s * , \s * / ) ;
587- if ( parts . length !== 4 ) {
588- return undefined ;
589- }
590- if ( parts . slice ( 0 , 3 ) . every ( percentRegEx . test . bind ( percentRegEx ) ) ) {
591- red = Math . floor ( ( parseFloat ( parts [ 0 ] . slice ( 0 , - 1 ) ) * 255 ) / 100 ) ;
592- green = Math . floor ( ( parseFloat ( parts [ 1 ] . slice ( 0 , - 1 ) ) * 255 ) / 100 ) ;
593- blue = Math . floor ( ( parseFloat ( parts [ 2 ] . slice ( 0 , - 1 ) ) * 255 ) / 100 ) ;
594- alpha = parseFloat ( parts [ 3 ] ) ;
595- } else if ( parts . slice ( 0 , 3 ) . every ( integerRegEx . test . bind ( integerRegEx ) ) ) {
596- red = parseInt ( parts [ 0 ] , 10 ) ;
597- green = parseInt ( parts [ 1 ] , 10 ) ;
598- blue = parseInt ( parts [ 2 ] , 10 ) ;
599- alpha = parseFloat ( parts [ 3 ] ) ;
600- } else {
602+ /**
603+ * <rgb()> | <rgba()>
604+ * <hsl()> | <hsla()>
605+ * <arg1>, <arg2>, <arg3>[, <alpha>]? or <arg1> <arg2> <arg3>[ / <alpha>]?
606+ * <alpha> should be <number> or <percentage>
607+ * <alpha> should be resolved to <number> and clamped to 0-1
608+ * value should be resolved to <rgb()> if <alpha> === 1
609+ */
610+ const fn = colorFnRegex . exec ( val ) ;
611+
612+ if ( fn ) {
613+ let [ , name , args ] = fn ;
614+ const [ [ arg1 , arg2 , arg3 , arg4 = 1 ] , [ sep1 , sep2 , sep3 ] ] = exports . splitFnArgs (
615+ args ,
616+ colorFnSeparators
617+ ) ;
618+ const alpha = exports . parseAlpha ( arg4 , true ) ;
619+
620+ name = name . toLowerCase ( ) ;
621+
622+ if (
623+ ! alpha ||
624+ sep1 !== sep2 ||
625+ ( ( sep3 && ! ( sep3 === ',' && sep1 === ',' ) ) || ( sep3 === '/' && sep1 === ' ' ) )
626+ ) {
601627 return undefined ;
602628 }
603- if ( isNaN ( alpha ) ) {
604- alpha = 1 ;
605- }
606- red = Math . min ( 255 , Math . max ( 0 , red ) ) ;
607- green = Math . min ( 255 , Math . max ( 0 , green ) ) ;
608- blue = Math . min ( 255 , Math . max ( 0 , blue ) ) ;
609- alpha = Math . min ( 1 , Math . max ( 0 , alpha ) ) ;
610- if ( alpha === 1 ) {
611- return 'rgb(' + red + ', ' + green + ', ' + blue + ')' ;
629+
630+ /**
631+ * <hsl()> | <hsla()>
632+ * <hue> should be <angle> or <number>
633+ * <hue> should be resolved to <number> and clamped to 0-360 (540 -> 180)
634+ * <saturation> and <lightness> should be <percentage> and clamped to 0-100%
635+ * value should be resolved to <rgb()> or <rgba()>
636+ */
637+ if ( name === 'hsl' ) {
638+ const hsl = [ ] ;
639+ let hue ;
640+ if ( ( hue = exports . parseNumber ( arg1 ) ) ) {
641+ hsl . push ( ( hue /= 60 ) ) ;
642+ } else if ( ( hue = exports . parseAngle ( arg1 , true ) ) ) {
643+ hsl . push ( hue . slice ( 0 , - 3 ) / 60 ) ;
644+ } else {
645+ return undefined ;
646+ }
647+ [ arg2 , arg3 ] . forEach ( val => {
648+ if ( ( val = exports . parsePercent ( val , true ) ) ) {
649+ return hsl . push ( Math . min ( 100 , Math . max ( 0 , val . slice ( 0 , - 1 ) ) ) / 100 ) ;
650+ }
651+ } ) ;
652+
653+ if ( hsl . length < 3 ) {
654+ return undefined ;
655+ }
656+
657+ rgb . push ( ...hslToRgb ( ...hsl ) ) ;
658+
659+ if ( alpha === '1' ) {
660+ return `rgb(${ rgb . join ( ', ' ) } )` ;
661+ }
662+ return `rgba(${ rgb . join ( ', ' ) } , ${ alpha } )` ;
663+ }
664+
665+ /**
666+ * <rgb()> | <rgba()>
667+ * rgb args should all be <number> or <percentage>
668+ * rgb args should be resolved to <number> and clamped to 0-255
669+ */
670+ if ( name === 'rgb' ) {
671+ const types = new Set ( ) ;
672+ [ arg1 , arg2 , arg3 ] . forEach ( val => {
673+ const number = exports . parseNumber ( val ) ;
674+ if ( number ) {
675+ types . add ( 'number' ) ;
676+ rgb . push ( Math . round ( Math . min ( 255 , Math . max ( 0 , number ) ) ) ) ;
677+ return ;
678+ }
679+ const percentage = exports . parsePercent ( val , true ) ;
680+ if ( percentage ) {
681+ types . add ( 'percent' ) ;
682+ rgb . push ( Math . round ( Math . min ( 255 , Math . max ( 0 , ( percentage . slice ( 0 , - 1 ) / 100 ) * 255 ) ) ) ) ;
683+ return ;
684+ }
685+ } ) ;
686+
687+ if ( rgb . length < 3 ) {
688+ return undefined ;
689+ }
690+
691+ if ( types . size > 1 ) {
692+ return undefined ;
693+ }
694+
695+ if ( alpha == 1 ) {
696+ return `rgb(${ rgb . join ( ', ' ) } )` ;
697+ }
698+ return `rgba(${ rgb . join ( ', ' ) } , ${ alpha } )` ;
612699 }
613- return 'rgba(' + red + ', ' + green + ', ' + blue + ', ' + alpha + ')' ;
614700 }
615701
616- res = colorRegEx4 . exec ( val ) ;
617- if ( res ) {
618- const [ , _hue , _saturation , _lightness , _alphaString = '' ] = res ;
619- const _alpha = parseFloat ( _alphaString . replace ( ',' , '' ) . trim ( ) ) ;
620- if ( ! _hue || ! _saturation || ! _lightness ) {
621- return undefined ;
702+ /**
703+ * <named-color> | <system-color> | currentcolor | transparent
704+ */
705+ return exports . parseKeyword ( val , namedColors ) ;
706+ } ;
707+
708+ /**
709+ * This function is used to split args from a CSS function that can have nested
710+ * functions which are sharing the same separator(s).
711+ */
712+ exports . splitFnArgs = function splitFnArgs ( val , separators = [ ',' ] ) {
713+ let argIndex = 0 ;
714+ let depth = 0 ;
715+
716+ const seps = [ ] ;
717+ const args = Array . from ( val ) . reduce ( ( args , char ) => {
718+ if ( char === '(' ) {
719+ depth ++ ;
720+ } else if ( char === ')' ) {
721+ depth -- ;
722+ } else if ( depth === 0 && separators . includes ( char ) ) {
723+ // Create empty arg except if separator is a space
724+ if ( args [ argIndex ] === undefined ) {
725+ if ( char === ' ' ) {
726+ return args ;
727+ }
728+ if ( seps [ argIndex - 1 ] === ' ' ) {
729+ seps [ argIndex - 1 ] = char ;
730+ return args ;
731+ }
732+ args [ argIndex ] = '' ;
733+ }
734+ argIndex ++ ;
735+ seps . push ( char ) ;
736+ return args ;
622737 }
623- hue = parseFloat ( _hue ) ;
624- saturation = parseInt ( _saturation , 10 ) ;
625- lightness = parseInt ( _lightness , 10 ) ;
626- if ( _alpha && numberRegEx . test ( _alpha ) ) {
627- alpha = parseFloat ( _alpha ) ;
738+ if ( args [ argIndex ] === undefined ) {
739+ args . push ( char ) ;
740+ } else {
741+ args [ argIndex ] += char ;
628742 }
743+ return args ;
744+ } , [ ] ) ;
629745
630- const [ r , g , b ] = hslToRgb ( hue , saturation / 100 , lightness / 100 ) ;
631- if ( ! _alphaString || alpha === 1 ) {
632- return 'rgb(' + r + ', ' + g + ', ' + b + ')' ;
633- }
634- return 'rgba(' + r + ', ' + g + ', ' + b + ', ' + alpha + ')' ;
746+ if ( args . length === seps . length ) {
747+ args . push ( '' ) ;
635748 }
636749
637- return exports . parseKeyword ( val , namedColors ) ;
750+ return [ args . map ( a => a . trim ( '' ) ) , seps ] ;
638751} ;
639752
640753// utility to translate from border-width to borderWidth
0 commit comments