@@ -49,29 +49,14 @@ function getKeywordColor(value: unknown): KeywordColor | null {
4949 return null
5050}
5151
52- // https://github.com/khalilgharbaoui/coloregex
53- const colorRegex = new RegExp (
54- `(?:^|\\s|\\(|,)(#(?:[0-9a-f]{2}){2,4}|(#[0-9a-f]{3})|(rgba?|hsla?|(?:ok)?(?:lab|lch))\\(\\s*(-?[\\d.]+(%|deg|rad|grad|turn)?(\\s*[,/]\\s*|\\s+)+){2,3}\\s*([\\d.]+(%|deg|rad|grad|turn)?|var\\([^)]+\\))?\\)|transparent|currentColor|${ Object . keys (
55- namedColors ,
56- ) . join ( '|' ) } )(?:$|\\s|\\)|,)`,
57- 'gi' ,
58- )
59-
6052function getColorsInString ( state : State , str : string ) : ParsedColor [ ] {
6153 if ( / (?: b o x | d r o p ) - s h a d o w / . test ( str ) && ! / - - t w - d r o p - s h a d o w / . test ( str ) ) return [ ]
6254
63- function toColor ( match : RegExpMatchArray ) {
64- let color = match [ 1 ] . replace ( / v a r \( [ ^ ) ] + \) / , '1' )
65- return getKeywordColor ( color ) ?? tryParseColor ( color )
66- }
67-
6855 str = replaceCssVarsWithFallbacks ( state , str )
6956 str = removeColorMixWherePossible ( str )
7057 str = resolveLightDark ( str )
7158
72- let possibleColors = str . matchAll ( colorRegex )
73-
74- return Array . from ( possibleColors , toColor ) . filter ( Boolean )
59+ return parseColors ( str )
7560}
7661
7762function getColorFromDecls (
@@ -335,3 +320,227 @@ const LIGHT_DARK_REGEX = /light-dark\(\s*(.*?)\s*,\s*.*?\s*\)/g
335320function resolveLightDark ( str : string ) {
336321 return str . replace ( LIGHT_DARK_REGEX , ( _ , lightColor ) => lightColor )
337322}
323+
324+ const COLOR_FNS = new Set ( [
325+ //
326+ 'rgb' ,
327+ 'rgba' ,
328+ 'hwb' ,
329+ 'hsl' ,
330+ 'hsla' ,
331+ 'lab' ,
332+ 'lch' ,
333+ 'oklab' ,
334+ 'oklch' ,
335+ 'color' ,
336+ ] )
337+
338+ const COLOR_NAMES = new Set ( [
339+ ...Object . keys ( namedColors ) . map ( ( c ) => c . toLowerCase ( ) ) ,
340+ 'transparent' ,
341+ 'currentcolor' ,
342+ ] )
343+
344+ const CSS_VARS = / v a r \( [ ^ ) ] + \) /
345+ const COLOR_FN_ARGS =
346+ / ^ \s * (?: (?: - ? [ \d . ] + (?: % | d e g | g ? r a d | t u r n ) ? | v a r \( [ ^ ) ] + \) ) (?: \s * [ , / ] \s * | \s + ) ) { 2 , 3 } (?: - ? [ \d . ] + (?: % | d e g | g ? r a d | t u r n ) ? | v a r \( [ ^ ) ] + \) ) \s * $ / i
347+
348+ const POUND = 0x23
349+ const ZERO = 0x30
350+ const NINE = 0x39
351+ const DOUBLE_QUOTE = 0x22
352+ const SINGLE_QUOTE = 0x27
353+ const BACKSLASH = 0x5c
354+ const LOWER_A = 0x61
355+ const LOWER_F = 0x66
356+ const LOWER_Z = 0x7a
357+ const L_PAREN = 0x28
358+ const R_PAREN = 0x29
359+ const SPACE = 0x20
360+ const COMMA = 0x2c
361+ const DASH = 0x2d
362+ const LINE_BREAK = 0x0a
363+ const CARRIAGE_RETURN = 0xd
364+ const TAB = 0x09
365+
366+ type Span = [ start : number , end : number ]
367+
368+ function maybeFindColors ( input : string ) : Span [ ] {
369+ let colors : Span [ ] = [ ]
370+ let len = input . length
371+
372+ for ( let i = 0 ; i < len ; ++ i ) {
373+ let char = input . charCodeAt ( i )
374+ let inner = char
375+
376+ if ( char >= LOWER_A && char <= LOWER_Z ) {
377+ // Read until we don't have a named color character
378+ let start = i
379+ let end = i
380+
381+ for ( let j = start + 1 ; j < len ; j ++ ) {
382+ inner = input . charCodeAt ( j )
383+
384+ if ( inner >= ZERO && inner <= NINE ) {
385+ end = j // 0-9
386+ } else if ( inner >= LOWER_A && inner <= LOWER_Z ) {
387+ end = j // a-z
388+ } else if ( inner === DASH ) {
389+ end = j // -
390+ } else if ( inner === L_PAREN ) {
391+ // Start of a function
392+ break
393+ } else if (
394+ inner === COMMA ||
395+ inner === SPACE ||
396+ inner === LINE_BREAK ||
397+ inner === TAB ||
398+ inner === CARRIAGE_RETURN ||
399+ inner === R_PAREN
400+ ) {
401+ // (?=$|[\\s),])
402+ break
403+ } else {
404+ end = i
405+ break
406+ }
407+ }
408+
409+ let name = input . slice ( start , end + 1 )
410+
411+ if ( COLOR_NAMES . has ( name ) ) {
412+ i = end
413+ colors . push ( [ start , end + 1 ] )
414+ continue
415+ }
416+
417+ if ( inner === L_PAREN && COLOR_FNS . has ( name ) ) {
418+ // Scan until the next balanced R_PAREN
419+ let depth = 1
420+ let argStart = end + 2
421+
422+ for ( let j = argStart ; j < len ; ++ j ) {
423+ inner = input . charCodeAt ( j )
424+
425+ // The next character is escaped, so we skip it.
426+ if ( inner === BACKSLASH ) {
427+ j += 1
428+ }
429+
430+ // Strings should be handled as-is until the end of the string. No need to
431+ // worry about balancing parens, brackets, or curlies inside a string.
432+ else if ( inner === SINGLE_QUOTE || inner === DOUBLE_QUOTE ) {
433+ // Ensure we don't go out of bounds.
434+ while ( ++ j < len ) {
435+ let nextChar = input . charCodeAt ( j )
436+
437+ // The next character is escaped, so we skip it.
438+ if ( nextChar === BACKSLASH ) {
439+ j += 1
440+ continue
441+ }
442+
443+ if ( nextChar === char ) {
444+ break
445+ }
446+ }
447+ }
448+
449+ // Track opening parens
450+ else if ( inner === L_PAREN ) {
451+ depth ++
452+ }
453+
454+ // Track closing parens
455+ else if ( inner === R_PAREN ) {
456+ depth --
457+ }
458+
459+ if ( depth > 0 ) continue
460+
461+ let args = input . slice ( argStart , j )
462+
463+ if ( ! COLOR_FN_ARGS . test ( args ) ) continue
464+ colors . push ( [ start , j + 1 ] )
465+ i = j + 1
466+
467+ break
468+ }
469+
470+ continue
471+ }
472+
473+ i = end
474+ }
475+
476+ //
477+ else if ( char === POUND ) {
478+ // Read until we don't have a named color character
479+ let start = i
480+ let end = i
481+
482+ // i + 1 = first hex digit
483+ // i + 1 + 8 = one past the last hex digit
484+ let last = Math . min ( start + 1 + 8 , len )
485+
486+ for ( let j = start + 1 ; j < last ; j ++ ) {
487+ let inner = input . charCodeAt ( j )
488+
489+ if ( inner >= ZERO && inner <= NINE ) {
490+ end = j // 0-9
491+ } else if ( inner >= LOWER_A && inner <= LOWER_F ) {
492+ end = j // a-f
493+ } else if (
494+ inner === COMMA ||
495+ inner === SPACE ||
496+ inner === TAB ||
497+ inner === LINE_BREAK ||
498+ inner === CARRIAGE_RETURN ||
499+ inner === R_PAREN
500+ ) {
501+ // (?=$|[\\s),])
502+ break
503+ } else {
504+ end = start
505+ break
506+ }
507+ }
508+
509+ let hexLen = end - start
510+ i = end
511+
512+ if ( hexLen === 3 || hexLen === 4 || hexLen === 6 || hexLen === 8 ) {
513+ colors . push ( [ start , end + 1 ] )
514+ continue
515+ }
516+ }
517+ }
518+
519+ return colors
520+ }
521+
522+ export function findColors ( input : string ) : string [ ] {
523+ return maybeFindColors ( input . toLowerCase ( ) ) . map ( ( [ start , end ] ) => input . slice ( start , end ) )
524+ }
525+
526+ export function parseColors ( input : string ) : ParsedColor [ ] {
527+ let colors : ParsedColor [ ] = [ ]
528+
529+ for ( let str of findColors ( input ) ) {
530+ str = str . replace ( CSS_VARS , '1' )
531+
532+ let keyword = getKeywordColor ( str )
533+ if ( keyword ) {
534+ colors . push ( keyword )
535+ continue
536+ }
537+
538+ let color = tryParseColor ( str )
539+ if ( color ) {
540+ colors . push ( color )
541+ continue
542+ }
543+ }
544+
545+ return colors
546+ }
0 commit comments