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

93.15% Statements 68/73
82.6% Branches 38/46
100% Functions 11/11
95.52% Lines 64/67

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                      1x                 2x                   24x 8x         16x 16x                     8x 8x 24x     24x   24x 24x 24x   24x     8x                 8x 7x       1x     1x 1x 1x 8x 8x 8x 1x 1x       1x       1x                           7x 7x               7x 7x 1x           6x                   8x           8x 8x 1x 1x     7x 7x 1x     1x         8x 1x       8x 2x     8x   8x             1x 11x   11x 10x     10x 2x     8x 8x 8x     11x               1x 3x     3x   3x             1x      
import type { Dictionary, FormatFn, TransformedToken } from 'style-dictionary/types';
 
import type { StyleDictionaryHost } from '../style-dictionary-host.js';
import { flatTokenMetadataFields } from '../utils/flat-token-fields.js';
import { MODE_EXTENSION_KEY } from '../utils/constants.js';
 
/**
 * Custom format for Design Token raw format
 * Outputs to {theme}.tokens.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) {
      Eif (!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++) {
    Iif (content[i] === '(') depth++;
    else Iif (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: TransformedToken) {
  // Check for light/dark object format in original value
  const originalValue = token.original?.$value;
  Iif (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: TransformedToken, tokenValue) {
  const tokenObject: Record<string, unknown> = {
    $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) {
      Iif (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;
  }
 
  Object.assign(tokenObject, flatTokenMetadataFields(token));
 
  return tokenObject;
}
 
// ============================================================================
// MAIN FORMATTER
// ============================================================================
 
const jsonRawFormatter = (dictionary: 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: StyleDictionaryHost) => {
  const formatFn = (({ dictionary }) => jsonRawFormatter(dictionary)) satisfies FormatFn;
 
  // Avoid nested collision warnings - this formatter creates a nested structure
  const format = Object.assign(formatFn, { nested: true });
 
  StyleDictionary.registerFormat({
    name: 'json/raw',
    format,
  });
};
 
// Export internal functions for testing
export const _testExports = {
  jsonRawFormatter,
};