@@ -49,7 +49,6 @@ const integerPattern = '[-+]?\\d+';
49
49
const numberPattern = `((${ integerPattern } )(\\.\\d+)?|[-+]?(\\.\\d+))(e[-+]?${ integerPattern } )?` ;
50
50
const percentPattern = `(${ numberPattern } )(%)` ;
51
51
const identRegEx = new RegExp ( `^${ identPattern } $` , 'i' ) ;
52
- const integerRegEx = new RegExp ( `^${ integerPattern } $` ) ;
53
52
const numberRegEx = new RegExp ( `^${ numberPattern } $` ) ;
54
53
const percentRegEx = new RegExp ( `^${ percentPattern } $` ) ;
55
54
const stringRegEx = / ^ ( " [ ^ " ] * " | ' [ ^ ' ] * ' ) $ / ;
@@ -64,10 +63,9 @@ const anglePattern = `(${numberPattern})(deg|grad|rad|turn)`;
64
63
const lengthPattern = `(${ numberPattern } )(ch|cm|r?em|ex|in|lh|mm|pc|pt|px|q|vh|vmin|vmax|vw)` ;
65
64
const angleRegEx = new RegExp ( `^${ anglePattern } $` , 'i' ) ;
66
65
const 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;
71
69
const lengthRegEx = new RegExp ( `^${ lengthPattern } $` , 'i' ) ;
72
70
const numericRegEx = new RegExp ( `^(${ numberPattern } )(%|${ identPattern } )?$` , 'i' ) ;
73
71
const timeRegEx = new RegExp ( `^(${ numberPattern } )(m?s)$` , 'i' ) ;
@@ -220,6 +218,41 @@ exports.parseMeasurement = function parseMeasurement(val) {
220
218
return exports . parsePercent ( val ) ;
221
219
} ;
222
220
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
+
223
256
/**
224
257
* https://drafts.csswg.org/css-values-4/#angles
225
258
* https://drafts.csswg.org/cssom/#ref-for-angle-value
@@ -526,115 +559,195 @@ exports.parseColor = function parseColor(val) {
526
559
if ( val === '' ) {
527
560
return val ;
528
561
}
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 ) ) ;
555
562
556
- return 'rgba(' + red + ', ' + green + ', ' + blue + ', ' + hexAlphaToRgbaAlpha + ')' ;
557
- }
558
- return 'rgb(' + red + ', ' + green + ', ' + blue + ')' ;
559
- }
563
+ const rgb = [ ] ;
560
564
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 ;
566
594
}
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 ( ', ' ) } )` ;
577
598
}
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 ) } )` ;
582
600
}
583
601
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
+ ) {
601
627
return undefined ;
602
628
}
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 } )` ;
612
699
}
613
- return 'rgba(' + red + ', ' + green + ', ' + blue + ', ' + alpha + ')' ;
614
700
}
615
701
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 ;
622
737
}
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 ;
628
742
}
743
+ return args ;
744
+ } , [ ] ) ;
629
745
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 ( '' ) ;
635
748
}
636
749
637
- return exports . parseKeyword ( val , namedColors ) ;
750
+ return [ args . map ( a => a . trim ( '' ) ) , seps ] ;
638
751
} ;
639
752
640
753
// utility to translate from border-width to borderWidth
0 commit comments