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