@@ -319,7 +319,15 @@ export function normalizeColor(color: unknown): number | null {
319319 return null ;
320320 }
321321
322- const input = color . trim ( ) ;
322+ let inputUntrimmed = color ;
323+ while ( inputUntrimmed . includes ( ' ' ) ) {
324+ inputUntrimmed = inputUntrimmed . replace ( ' ' , ' ' ) ;
325+ }
326+
327+ const input = inputUntrimmed . trim ( ) ;
328+ if ( input . length > 0 && inputUntrimmed [ 0 ] === ' ' && input [ 0 ] === '#' ) {
329+ return null ;
330+ }
323331
324332 function isAllHexDigits ( str : string ) : boolean {
325333 for ( let i = 0 ; i < str . length ; i ++ ) {
@@ -335,8 +343,39 @@ export function normalizeColor(color: unknown): number | null {
335343 return true ;
336344 }
337345
338- if ( names [ input . toLowerCase ( ) ] !== undefined ) {
339- return names [ input . toLowerCase ( ) ] ;
346+ function isAllDigits ( str : string ) : boolean {
347+ for ( let i = 0 ; i < str . length ; i ++ ) {
348+ const c = str [ i ] ;
349+ if ( i === 0 && ( c === '-' || c === '+' ) ) {
350+ continue ;
351+ }
352+
353+ const isNum = c >= '0' && c <= '9' ;
354+ if ( ! isNum ) {
355+ return false ;
356+ }
357+ }
358+ return true ;
359+ }
360+
361+ function isAllDigitsDot ( str : string ) : boolean {
362+ const newStr = str . replace ( '.' , '' ) ; // only remove one '.'
363+ return isAllDigits ( newStr ) ;
364+ }
365+
366+ function isPercentage ( str : string ) : boolean {
367+ if ( ! str . includes ( '%' ) ) {
368+ return false ;
369+ }
370+ const digitDot = str . replace ( '%' , '' ) ;
371+ if ( isAllDigitsDot ( digitDot ) ) {
372+ return true ;
373+ }
374+ return false ;
375+ }
376+
377+ if ( names [ input ] !== undefined ) {
378+ return names [ input ] ;
340379 }
341380
342381 // #RRGGBB => 7 chars total, e.g. "#1a2B3C"
@@ -346,19 +385,29 @@ export function normalizeColor(color: unknown): number | null {
346385 return Number . parseInt ( hexPart + 'ff' , 16 ) >>> 0 ;
347386 }
348387 }
349-
350- // rgb(R, G, B)
388+ // rgb(R, G, B) or rgb(R G B)
351389 if ( input . startsWith ( 'rgb(' ) && input . endsWith ( ')' ) ) {
352390 const inside = input . slice ( 4 , - 1 ) . trim ( ) ;
353- const parts = inside . split ( ',' ) . map ( p => p . trim ( ) ) ;
354- if ( parts . length === 3 ) {
355- const r = parse255 ( parts [ 0 ] ) ;
356- const g = parse255 ( parts [ 1 ] ) ;
357- const b = parse255 ( parts [ 2 ] ) ;
358- if ( r != null && g != null && b != null ) {
359- return ( ( r << 24 ) | ( g << 16 ) | ( b << 8 ) | 0xff ) >>> 0 ;
391+ let parts = inside . split ( ',' ) . map ( ( p ) => p . trim ( ) ) ;
392+
393+ if ( parts . length !== 3 ) {
394+ parts = inside . split ( ' ' ) . map ( ( p ) => p . trim ( ) ) ;
395+ if ( parts . length !== 3 ) {
396+ return null ;
397+ }
398+ }
399+ for ( const part of parts ) {
400+ if ( ! isAllDigitsDot ( part ) ) {
401+ return null ;
360402 }
361403 }
404+
405+ const r = parse255 ( parts [ 0 ] ) ;
406+ const g = parse255 ( parts [ 1 ] ) ;
407+ const b = parse255 ( parts [ 2 ] ) ;
408+ if ( r != null && g != null && b != null ) {
409+ return ( ( r << 24 ) | ( g << 16 ) | ( b << 8 ) | 0xff ) >>> 0 ;
410+ }
362411 }
363412
364413 // rgba(R, G, B, A) or rgba(R G B / A)
@@ -368,8 +417,20 @@ export function normalizeColor(color: unknown): number | null {
368417 // slash form
369418 const [ beforeSlash , alphaPart ] = inside . split ( '/' ) ;
370419 if ( beforeSlash && alphaPart ) {
371- const rgbParts = beforeSlash . trim ( ) . split ( ' ' ) . map ( x => x . trim ( ) ) ;
420+ const rgbParts = beforeSlash
421+ . trim ( )
422+ . split ( ' ' )
423+ . map ( ( x ) => x . trim ( ) ) ;
372424 if ( rgbParts . length === 3 ) {
425+ for ( const rgbPart of rgbParts ) {
426+ if ( ! isAllDigitsDot ( rgbPart ) ) {
427+ return null ;
428+ }
429+ }
430+ if ( ! isAllDigitsDot ( alphaPart . trim ( ) ) ) {
431+ return null ;
432+ }
433+
373434 const r = parse255 ( rgbParts [ 0 ] ) ;
374435 const g = parse255 ( rgbParts [ 1 ] ) ;
375436 const b = parse255 ( rgbParts [ 2 ] ) ;
@@ -381,8 +442,13 @@ export function normalizeColor(color: unknown): number | null {
381442 }
382443 } else {
383444 // comma form
384- const parts = inside . split ( ',' ) . map ( p => p . trim ( ) ) ;
445+ const parts = inside . split ( ',' ) . map ( ( p ) => p . trim ( ) ) ;
385446 if ( parts . length === 4 ) {
447+ for ( const part of parts ) {
448+ if ( ! isAllDigitsDot ( part ) ) {
449+ return null ;
450+ }
451+ }
386452 const r = parse255 ( parts [ 0 ] ) ;
387453 const g = parse255 ( parts [ 1 ] ) ;
388454 const b = parse255 ( parts [ 2 ] ) ;
@@ -400,9 +466,12 @@ export function normalizeColor(color: unknown): number | null {
400466 if ( shortHex . length === 3 && isAllHexDigits ( shortHex ) ) {
401467 // Expand => "FF00cc" + "ff"
402468 const expanded =
403- shortHex [ 0 ] + shortHex [ 0 ] +
404- shortHex [ 1 ] + shortHex [ 1 ] +
405- shortHex [ 2 ] + shortHex [ 2 ] +
469+ shortHex [ 0 ] +
470+ shortHex [ 0 ] +
471+ shortHex [ 1 ] +
472+ shortHex [ 1 ] +
473+ shortHex [ 2 ] +
474+ shortHex [ 2 ] +
406475 'ff' ;
407476 return Number . parseInt ( expanded , 16 ) >>> 0 ;
408477 }
@@ -421,27 +490,46 @@ export function normalizeColor(color: unknown): number | null {
421490 const shortHex = input . slice ( 1 ) ; // e.g. "F0cF"
422491 if ( shortHex . length === 4 && isAllHexDigits ( shortHex ) ) {
423492 const expanded =
424- shortHex [ 0 ] + shortHex [ 0 ] +
425- shortHex [ 1 ] + shortHex [ 1 ] +
426- shortHex [ 2 ] + shortHex [ 2 ] +
427- shortHex [ 3 ] + shortHex [ 3 ] ;
493+ shortHex [ 0 ] +
494+ shortHex [ 0 ] +
495+ shortHex [ 1 ] +
496+ shortHex [ 1 ] +
497+ shortHex [ 2 ] +
498+ shortHex [ 2 ] +
499+ shortHex [ 3 ] +
500+ shortHex [ 3 ] ;
428501 return Number . parseInt ( expanded , 16 ) >>> 0 ;
429502 }
430503 }
431504
432- // hsl(H, S%, L%)
505+ // hsl(H, S%, L%) or hsl(H S% L%)
433506 if ( input . startsWith ( 'hsl(' ) && input . endsWith ( ')' ) ) {
434507 const inside = input . slice ( 4 , - 1 ) . trim ( ) ;
435- const parts = inside . split ( ',' ) . map ( p => p . trim ( ) ) ;
436- if ( parts . length === 3 ) {
437- const h = parse360 ( parts [ 0 ] ) ; // can be negative, wraps via mod
438- const s = parsePercentage ( parts [ 1 ] ) ;
439- const l = parsePercentage ( parts [ 2 ] ) ;
440- if ( h != null && s != null && l != null ) {
441- const rgb = hslToRgb ( h , s , l ) ;
442- return ( rgb | 0xff ) >>> 0 ; // alpha=255
508+ let parts = inside . split ( ',' ) . map ( ( p ) => p . trim ( ) ) ;
509+
510+ if ( parts . length !== 3 ) {
511+ parts = inside . split ( ' ' ) . map ( ( p ) => p . trim ( ) ) ;
512+ if ( parts . length !== 3 ) {
513+ return null ;
443514 }
444515 }
516+ if ( ! isAllDigitsDot ( parts [ 0 ] ) ) {
517+ return null ;
518+ }
519+ if ( ! isPercentage ( parts [ 1 ] ) ) {
520+ return null ;
521+ }
522+ if ( ! isPercentage ( parts [ 2 ] ) ) {
523+ return null ;
524+ }
525+
526+ const h = parse360 ( parts [ 0 ] ) ; // can be negative, wraps via mod
527+ const s = parsePercentage ( parts [ 1 ] ) ;
528+ const l = parsePercentage ( parts [ 2 ] ) ;
529+ if ( h != null && s != null && l != null ) {
530+ const rgb = hslToRgb ( h , s , l ) ;
531+ return ( rgb | 0xff ) >>> 0 ; // alpha=255
532+ }
445533 }
446534
447535 // hsla(H, S%, L%, A) or hsla(H S% L% / A)
@@ -451,8 +539,24 @@ export function normalizeColor(color: unknown): number | null {
451539 // slash form => "H, S%, L% / A"
452540 const [ beforeSlash , alphaPart ] = inside . split ( '/' ) ;
453541 if ( beforeSlash && alphaPart ) {
454- const hslParts = beforeSlash . split ( ',' ) . map ( p => p . trim ( ) ) ;
542+ const hslParts = beforeSlash
543+ . trim ( )
544+ . split ( ' ' )
545+ . map ( ( p ) => p . trim ( ) ) ;
455546 if ( hslParts . length === 3 ) {
547+ if ( ! isAllDigitsDot ( hslParts [ 0 ] ) ) {
548+ return null ;
549+ }
550+ if ( ! isPercentage ( hslParts [ 1 ] ) ) {
551+ return null ;
552+ }
553+ if ( ! isPercentage ( hslParts [ 2 ] ) ) {
554+ return null ;
555+ }
556+ if ( ! isAllDigitsDot ( alphaPart . trim ( ) ) ) {
557+ return null ;
558+ }
559+
456560 const h = parse360 ( hslParts [ 0 ] ) ;
457561 const s = parsePercentage ( hslParts [ 1 ] ) ;
458562 const l = parsePercentage ( hslParts [ 2 ] ) ;
@@ -465,8 +569,21 @@ export function normalizeColor(color: unknown): number | null {
465569 }
466570 } else {
467571 // comma form => "H, S%, L%, A"
468- const parts = inside . split ( ',' ) . map ( p => p . trim ( ) ) ;
572+ const parts = inside . split ( ',' ) . map ( ( p ) => p . trim ( ) ) ;
469573 if ( parts . length === 4 ) {
574+ if ( ! isAllDigitsDot ( parts [ 0 ] ) ) {
575+ return null ;
576+ }
577+ if ( ! isPercentage ( parts [ 1 ] ) ) {
578+ return null ;
579+ }
580+ if ( ! isPercentage ( parts [ 2 ] ) ) {
581+ return null ;
582+ }
583+ if ( ! isAllDigitsDot ( parts [ 3 ] ) ) {
584+ return null ;
585+ }
586+
470587 const h = parse360 ( parts [ 0 ] ) ;
471588 const s = parsePercentage ( parts [ 1 ] ) ;
472589 const l = parsePercentage ( parts [ 2 ] ) ;
@@ -479,19 +596,34 @@ export function normalizeColor(color: unknown): number | null {
479596 }
480597 }
481598
482- // hwb(H, W%, B%) – angle can be negative
599+ // hwb(H, W%, B%) or hwb(H W% B%) -- angle can be negative
483600 if ( input . startsWith ( 'hwb(' ) && input . endsWith ( ')' ) ) {
484601 const inside = input . slice ( 4 , - 1 ) . trim ( ) ;
485- const parts = inside . split ( ',' ) . map ( p => p . trim ( ) ) ;
486- if ( parts . length === 3 ) {
487- const h = parse360 ( parts [ 0 ] ) ;
488- const w = parsePercentage ( parts [ 1 ] ) ;
489- const b = parsePercentage ( parts [ 2 ] ) ;
490- if ( h != null && w != null && b != null ) {
491- const rgb = hwbToRgb ( h , w , b ) ;
492- return ( rgb | 0xff ) >>> 0 ; // alpha=255
602+ let parts = inside . split ( ',' ) . map ( ( p ) => p . trim ( ) ) ;
603+
604+ if ( parts . length !== 3 ) {
605+ parts = inside . split ( ' ' ) . map ( ( p ) => p . trim ( ) ) ;
606+ if ( parts . length !== 3 ) {
607+ return null ;
493608 }
494609 }
610+ if ( ! isAllDigitsDot ( parts [ 0 ] ) ) {
611+ return null ;
612+ }
613+ if ( ! isPercentage ( parts [ 1 ] ) ) {
614+ return null ;
615+ }
616+ if ( ! isPercentage ( parts [ 2 ] ) ) {
617+ return null ;
618+ }
619+
620+ const h = parse360 ( parts [ 0 ] ) ;
621+ const w = parsePercentage ( parts [ 1 ] ) ;
622+ const b = parsePercentage ( parts [ 2 ] ) ;
623+ if ( h != null && w != null && b != null ) {
624+ const rgb = hwbToRgb ( h , w , b ) ;
625+ return ( rgb | 0xff ) >>> 0 ; // alpha=255
626+ }
495627 }
496628
497629 // Nothing matched => invalid
0 commit comments