All files / packages/design-tokens/src/validators/cross-platform-colors css-parser.js

86.66% Statements 52/60
76% Branches 38/50
100% Functions 6/6
95.83% Lines 46/48

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 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128                            25x 25x 127x 127x   52x 52x   52x 52x   52x   25x                         49x 40x       40x     9x 9x   9x       9x   7x 7x   7x 7x         32x 32x               4x 4x 3x                 34x   34x 2x 2x 2x 2x 2x 2x 2x     32x 30x 30x                       25x 25x   25x 39x   38x 4x   34x       25x    
/**
 * Parser for SLDS CSS custom property files.
 * Extracts color tokens from --slds-g-color-* and --slds-r-color-* properties,
 * resolves var() references and light-dark() functions, and outputs normalized hex values.
 */
 
import { MODE, normalizeHex, toHex, parseLightDarkFunction } from './color-utils.js';
 
/**
 * Scan CSS lines and collect all --slds-g-* and --slds-r-* custom properties.
 * @param {string[]} lines
 * @returns {Map<string, string>}
 */
function parseCssProperties(lines) {
  const props = new Map();
  for (const line of lines) {
    const trimmed = line.trim();
    if (!trimmed.startsWith('--slds-g-') && !trimmed.startsWith('--slds-r-')) continue;
 
    const colonIdx = trimmed.indexOf(':');
    Iif (colonIdx === -1) continue;
 
    const semiIdx = trimmed.indexOf(';', colonIdx);
    Iif (semiIdx === -1) continue;
 
    props.set(trimmed.substring(0, colonIdx).trim(), trimmed.substring(colonIdx + 1, semiIdx).trim());
  }
  return props;
}
 
/**
 * Recursively resolve a CSS var() reference to a concrete value.
 * Handles light-dark() extraction when a mode is specified.
 * @param {string} value
 * @param {Map<string, string>} allProperties
 * @param {Set<string>} visited - tracks visited refs to prevent infinite loops
 * @param {string|null} mode - MODE.LIGHT | MODE.DARK | null
 * @returns {string}
 */
function resolveVar(value, allProperties, visited = new Set(), mode = null) {
  if (!value.startsWith('var(')) {
    Iif (mode && value.includes('light-dark(')) {
      const parsed = parseLightDarkFunction(value);
      if (parsed) return mode === MODE.LIGHT ? parsed.lightValue : parsed.darkValue;
    }
    return value;
  }
 
  const varEnd = value.indexOf(')', value.indexOf('var('));
  Iif (varEnd === -1) return value;
 
  const refName = value
    .substring(value.indexOf('var(') + 4, varEnd)
    .split(',')[0]
    .trim();
  if (visited.has(refName)) return value;
 
  const refValue = allProperties.get(refName);
  Iif (!refValue) return value;
 
  visited.add(refName);
  return resolveVar(refValue, allProperties, visited, mode);
}
 
/** Store a hex color under both -light and -dark suffixed keys. */
function setLightDark(colors, tokenName, hex) {
  colors.set(`${tokenName}-${MODE.LIGHT}`, hex);
  colors.set(`${tokenName}-${MODE.DARK}`, hex);
}
 
/**
 * Process a --slds-r-color-* reference token: resolve to a direct hex value
 * (reference tokens don't use light-dark(), same value for both modes).
 */
function processReferenceToken(colors, tokenName, value, allProperties) {
  const resolvedHex = resolveVar(value, allProperties, new Set(), null);
  if (resolvedHex && /^#[0-9a-f]{6}$/i.test(resolvedHex)) {
    setLightDark(colors, tokenName, normalizeHex(resolvedHex));
  }
}
 
/**
 * Process a --slds-g-color-* global token: handles both light-dark() semantic
 * tokens (produces separate light/dark entries) and single static values.
 */
function processGlobalToken(colors, tokenName, value, allProperties) {
  const resolvedValue = resolveVar(value, allProperties);
 
  if (resolvedValue.includes('light-dark(')) {
    const parsed = parseLightDarkFunction(resolvedValue);
    Iif (!parsed) return;
    const lightHex = toHex(resolveVar(parsed.lightValue, allProperties, new Set(), MODE.LIGHT));
    const darkHex = toHex(resolveVar(parsed.darkValue, allProperties, new Set(), MODE.DARK));
    Eif (lightHex) colors.set(`${tokenName}-${MODE.LIGHT}`, lightHex);
    Eif (darkHex) colors.set(`${tokenName}-${MODE.DARK}`, darkHex);
    return;
  }
 
  if (resolvedValue && !resolvedValue.startsWith('var(')) {
    const hex = toHex(resolvedValue);
    if (hex) setLightDark(colors, tokenName, hex);
  }
}
 
/**
 * Parse all color tokens from concatenated CSS file content.
 * Performs two passes: first collects all custom properties for var() resolution,
 * then processes only color properties into normalized hex values.
 * @param {string} cssContent
 * @returns {Map<string, string>} token name → hex (e.g. 'color-brand-base-50-light' → 'aabbcc')
 */
export function parseColorsFromCSS(cssContent) {
  const colors = new Map();
  const allProperties = parseCssProperties(cssContent.split('\n'));
 
  for (const [propName, value] of allProperties.entries()) {
    if (!propName.startsWith('--slds-g-color') && !propName.startsWith('--slds-r-color')) continue;
 
    if (propName.startsWith('--slds-r-color-')) {
      processReferenceToken(colors, propName.replace('--slds-r-color-', 'color-'), value, allProperties);
    } else {
      processGlobalToken(colors, propName.replace('--slds-g-', ''), value, allProperties);
    }
  }
 
  return colors;
}