All files / packages/design-tokens/src/validators/cross-platform-colors color-utils.js

100% Statements 34/34
100% Branches 21/21
100% Functions 4/4
100% Lines 30/30

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88              7x           7x                 80x   80x 9x   27x   80x   80x                   34x 34x   1x                       9x 9x   7x   7x 7x 7x   7x 149x 149x 4x 145x 10x 6x 6x   4x 135x 6x       7x   5x          
/**
 * Shared color utility constants and pure functions used across all platform parsers.
 */
 
import Color from 'colorjs.io';
 
/** Mode string constants for light/dark theming. */
export const MODE = {
  LIGHT: 'light',
  DARK: 'dark',
};
 
/** Length of the 'light-dark(' prefix string, used in substring operations. */
export const LIGHT_DARK_PREFIX_LENGTH = 'light-dark('.length;
 
/**
 * Normalize a hex color string to a lowercase 6-character format (no '#').
 * Handles 3-char shorthand, 8-char AARRGGBB, and uppercase input.
 * @param {string} hex
 * @returns {string}
 */
export function normalizeHex(hex) {
  let n = hex.replace('#', '');
 
  if (n.length === 3)
    n = n
      .split('')
      .map((c) => c + c)
      .join('');
  if (n.length === 8) n = n.substring(2); // strip alpha from AARRGGBB
 
  return n.toLowerCase();
}
 
/**
 * Parse a CSS color string into a normalized 6-char lowercase hex value.
 * Returns null if parsing fails.
 * @param {string} colorValue
 * @returns {string|null}
 */
export function toHex(colorValue) {
  try {
    return normalizeHex(new Color(colorValue).to('srgb').toString({ format: 'hex' }));
  } catch {
    return null;
  }
}
 
/**
 * Parse a light-dark() CSS function and extract the light and dark argument strings.
 * Uses a single character-scan pass to locate both the comma and the closing paren,
 * correctly handling nested parentheses inside the arguments.
 * @param {string} value - CSS value containing light-dark(...)
 * @returns {{ lightValue: string, darkValue: string } | null}
 */
export function parseLightDarkFunction(value) {
  const startIdx = value.indexOf('light-dark(');
  if (startIdx === -1) return null;
 
  const afterStart = value.substring(startIdx + LIGHT_DARK_PREFIX_LENGTH);
 
  let commaIdx = -1;
  let closeIdx = -1;
  let parenDepth = 0;
 
  for (let i = 0; i < afterStart.length; i++) {
    const ch = afterStart[i];
    if (ch === '(') {
      parenDepth++;
    } else if (ch === ')') {
      if (parenDepth === 0) {
        closeIdx = i;
        break;
      }
      parenDepth--;
    } else if (ch === ',' && parenDepth === 0 && commaIdx === -1) {
      commaIdx = i;
    }
  }
 
  if (commaIdx === -1 || closeIdx === -1) return null;
 
  return {
    lightValue: afterStart.substring(0, commaIdx).trim(),
    darkValue: afterStart.substring(commaIdx + 1, closeIdx).trim(),
  };
}