All files / packages/design-tokens/src/style-dictionary/formats json-raw.js

98.68% Statements 75/76
98% Branches 49/50
100% Functions 11/11
98.57% Lines 69/70

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 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219                2x                 319x                   7170x 1639x         5531x 5603x                     1641x 1641x 7170x     7170x   7170x 7242x 653x   7242x     1641x                 1641x 1638x       3x     3x 3x 3x 56x 54x 52x 3x 3x       3x       3x                           1638x 1638x 1x             1637x 1637x 315x           1322x                   1641x           1641x 1641x 3x 3x     1638x 1638x 316x 1x   316x         1641x 167x       1641x 1500x       1641x 62x 13x   62x     1641x             2x 32x   32x 1643x     1643x 2x     1641x 1641x 1641x     32x               2x 11x     4x   4x             2x      
import StyleDictionary from 'style-dictionary';
import { MODE_EXTENSION_KEY } from '../utils/constants.js';
 
/**
 * Custom format for Design Token raw format
 * Outputs to {theme}.hooks.raw.json
 */
 
const LIGHT_DARK_PREFIX = 'light-dark(';
 
/**
 * Create the standard dark mode extension structure
 * Single source of truth for the $extensions["com.salesforce-ux.mode"].dark format
 * @param {*} darkValue - The dark mode value
 * @returns {object} - Extension object in DTCG format
 */
function createDarkModeExtension(darkValue) {
  return { [MODE_EXTENSION_KEY]: { dark: { $value: darkValue } } };
}
 
/**
 * Split camelCase string into separate words
 * @param {string} str - camelCase string (e.g., "electricBlue")
 * @returns {string[]} - Array of words (e.g., ["electric", "blue"])
 */
function splitCamelCase(str) {
  // Handle empty or single character strings
  if (!str || str.length <= 1) {
    return [str];
  }
 
  // Split on capital letters, but keep the capital letters with their following lowercase letters
  // Example: "electricBlue" -> ["electric", "Blue"] -> ["electric", "blue"]
  const parts = str.split(/(?=[A-Z])/);
  return parts.map((s) => s.charAt(0).toLowerCase() + s.slice(1));
}
 
/**
 * Navigate to a nested path in the tokens object, creating intermediate objects as needed
 * Splits camelCase path segments into separate nested levels
 * @param {object} tokens - Root tokens object
 * @param {string[]} path - Token path array
 * @returns {object} - The parent object where the token should be placed
 */
function navigateToPath(tokens, path) {
  let current = tokens;
  for (let i = 0; i < path.length - 1; i++) {
    const segment = path[i];
    // Split camelCase segments into separate levels
    // e.g., "electricBlue" becomes ["electric", "blue"]
    const segments = splitCamelCase(segment);
 
    for (const subSegment of segments) {
      if (!current[subSegment]) {
        current[subSegment] = {};
      }
      current = current[subSegment];
    }
  }
  return current;
}
 
/**
 * Parse a light-dark() CSS function and extract light/dark values
 * @param {string} value - CSS value potentially containing light-dark()
 * @returns {object|null} - { light, dark } values or null if not a light-dark() function
 */
function parseLightDarkValue(value) {
  if (typeof value !== 'string' || !value.startsWith(LIGHT_DARK_PREFIX)) {
    return null;
  }
 
  // Remove 'light-dark(' prefix and trailing ')'
  const content = value.slice(LIGHT_DARK_PREFIX.length, -1);
 
  // Find the comma that separates light and dark values (not inside nested var parens)
  let depth = 0;
  let splitIndex = -1;
  for (let i = 0; i < content.length; i++) {
    if (content[i] === '(') depth++;
    else if (content[i] === ')') depth--;
    else if (content[i] === ',' && depth === 0) {
      splitIndex = i;
      break;
    }
  }
 
  Iif (splitIndex === -1) {
    return null;
  }
 
  return {
    light: content.slice(0, splitIndex).trim(),
    dark: content.slice(splitIndex + 1).trim(),
  };
}
 
/**
 * Extract dark mode extension from token's original value or extensions
 * Only called if parseLightDarkValue didn't find a light-dark() function
 * @param {object} token - Style Dictionary token
 * @returns {object|null} - { lightValue, darkExtension } or null
 */
function extractDarkModeExtension(token) {
  // Check for light/dark object format in original value
  const originalValue = token.original?.$value;
  if (originalValue && typeof originalValue === 'object' && originalValue.light && originalValue.dark) {
    return {
      lightValue: originalValue.light,
      darkExtension: createDarkModeExtension(originalValue.dark),
    };
  }
 
  // Check for dark mode in $extensions
  const darkValue = token.original?.$extensions?.[MODE_EXTENSION_KEY]?.dark?.$value;
  if (darkValue) {
    return {
      lightValue: null, // Keep existing $value unchanged
      darkExtension: createDarkModeExtension(darkValue),
    };
  }
 
  return null;
}
 
/**
 * Build a DTCG-compliant token object
 * @param {object} token - Style Dictionary token
 * @param {*} tokenValue - Pre-computed token value (original or computed)
 * @returns {object} - Token object with $type, $value, and optional metadata
 */
function buildTokenObject(token, tokenValue) {
  const tokenObject = {
    $type: token.$type,
    $value: tokenValue,
  };
 
  // Parse light-dark() CSS function first
  const lightDark = parseLightDarkValue(tokenValue);
  if (lightDark) {
    tokenObject.$value = lightDark.light;
    tokenObject.$extensions = createDarkModeExtension(lightDark.dark);
  } else {
    // Only check other dark mode sources if not a light-dark() function
    const darkMode = extractDarkModeExtension(token);
    if (darkMode) {
      if (darkMode.lightValue !== null) {
        tokenObject.$value = darkMode.lightValue;
      }
      tokenObject.$extensions = darkMode.darkExtension;
    }
  }
 
  // Add deprecated flag
  if (token.$deprecated || token.original?.$deprecated) {
    tokenObject.$deprecated = true;
  }
 
  // Add description
  if (token.$description) {
    tokenObject.$description = token.$description;
  }
 
  // Add cssProperties from $extensions if present
  if (token.original?.$extensions?.cssProperties) {
    if (!tokenObject.$extensions) {
      tokenObject.$extensions = {};
    }
    tokenObject.$extensions.cssProperties = token.original.$extensions.cssProperties;
  }
 
  return tokenObject;
}
 
// ============================================================================
// MAIN FORMATTER
// ============================================================================
 
const jsonRawFormatter = (dictionary) => {
  const tokens = {};
 
  dictionary.allTokens.forEach((token) => {
    const tokenValue = token.original?.$value || token.value;
 
    // Skip tokens without values
    if (tokenValue === undefined || tokenValue === null) {
      return;
    }
 
    const current = navigateToPath(tokens, token.path);
    const tokenName = token.path[token.path.length - 1];
    current[tokenName] = buildTokenObject(token, tokenValue);
  });
 
  return JSON.stringify(tokens, null, 2);
};
 
/**
 * Style Dictionary registration for the JSON raw format
 *
 * @param {StyleDictionary} StyleDictionary
 */
export const jsonRawFormat = (StyleDictionary) => {
  const formatter = ({ dictionary }) => jsonRawFormatter(dictionary);
 
  // Avoid nested collision warnings - this formatter creates a nested structure
  formatter.nested = true;
 
  StyleDictionary.registerFormat({
    name: 'json/raw',
    format: formatter,
  });
};
 
// Export internal functions for testing
export const _testExports = {
  jsonRawFormatter,
};