All files / packages/design-tokens/src/style-dictionary/utils value-unit-helpers.js

97.82% Statements 45/46
98.24% Branches 56/57
100% Functions 7/7
97.77% Lines 44/45

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                                          8x 8x                 9365x                                   701x   701x 4x         697x 1x         696x 1x         695x 3x         692x 3x         689x 12x                               1589x     1589x 2x     1587x   685x 674x       902x 897x       5x                       636x 5x         631x   631x 8x     623x 623x   623x       623x                 11855x 11851x                   8x 2x     6x 6x                   675x 59x     616x 616x    
/**
 * Utility functions for handling value+unit tokens in both string and object formats
 * Supports DTCG dimensional and duration types
 */
 
/**
 * Supported value+unit types
 * Per DTCG spec:
 * - Dimension: https://www.designtokens.org/tr/2025.10/format/#dimension
 * - Duration: https://www.designtokens.org/tr/2025.10/format/#duration
 *
 * Dimensional units (length/size):
 * - rem: Relative to root font size (most common)
 * - px: Absolute pixels
 * - ch: Character width (used in deprecated tokens)
 * - %: Percentage (used for circle radius)
 *
 * Duration units (time):
 * - s: Seconds
 * - ms: Milliseconds
 */
const SUPPORTED_UNITS = ['rem', 'px', 'ch', '%', 's', 'ms'];
const SUPPORTED_UNITS_REGEX = 'rem|px|ch|%|s|ms';
 
/**
 * Check if a value is a value+unit object with value and unit properties
 * Per DTCG spec: dimension/duration values MUST be an object with {value: number, unit: string}
 * @param {*} value - The value to check
 * @returns {boolean} - True if value is a value+unit object
 */
export function isValueUnitObject(value) {
  return (
    value &&
    typeof value === 'object' &&
    !Array.isArray(value) &&
    'value' in value &&
    'unit' in value &&
    typeof value.value === 'number' &&
    typeof value.unit === 'string'
  );
}
 
/**
 * Validate a value+unit object against DTCG spec requirements
 * @param {object} value - The value+unit object to validate
 * @param {string} tokenPath - Token path for error messages (optional)
 * @throws {Error} If the value+unit object is invalid
 */
export function validateValueUnitObject(value, tokenPath = '') {
  const pathInfo = tokenPath ? ` in token "${tokenPath}"` : '';
 
  if (!value || typeof value !== 'object' || Array.isArray(value)) {
    throw new Error(
      `Invalid value+unit${pathInfo}: Expected object with {value, unit}, got ${JSON.stringify(value)}`,
    );
  }
 
  if (!('value' in value)) {
    throw new Error(
      `Invalid value+unit${pathInfo}: Missing required "value" property. Got ${JSON.stringify(value)}`,
    );
  }
 
  if (!('unit' in value)) {
    throw new Error(
      `Invalid value+unit${pathInfo}: Missing required "unit" property. Got ${JSON.stringify(value)}`,
    );
  }
 
  if (typeof value.value !== 'number') {
    throw new Error(
      `Invalid value+unit${pathInfo}: Property "value" must be a number, got ${typeof value.value}: ${JSON.stringify(value.value)}`,
    );
  }
 
  if (typeof value.unit !== 'string') {
    throw new Error(
      `Invalid value+unit${pathInfo}: Property "unit" must be a string, got ${typeof value.unit}: ${JSON.stringify(value.unit)}`,
    );
  }
 
  if (!SUPPORTED_UNITS.includes(value.unit)) {
    throw new Error(
      `Invalid value+unit${pathInfo}: Unsupported unit "${value.unit}". Supported units: ${SUPPORTED_UNITS.join(', ')}`,
    );
  }
}
 
/**
 * Convert a value+unit value to string format
 * Handles both object format {value: 0.25, unit: "rem"} and string format "0.25rem"
 * Per DTCG spec, validates value+unit objects strictly
 * @param {string|object} value - The value+unit value
 * @param {string} tokenPath - Token path for error messages (optional)
 * @returns {string} - The value as a string (e.g., "0.25rem")
 * @throws {Error} If value is null, undefined, or invalid
 */
export function valueUnitToString(value, tokenPath = '') {
  const pathInfo = tokenPath ? ` in token "${tokenPath}"` : '';
 
  // Validate input
  if (value === null || value === undefined) {
    throw new Error(`valueUnitToString: value cannot be null or undefined${pathInfo}`);
  }
 
  if (isValueUnitObject(value)) {
    // Validate against DTCG spec requirements
    validateValueUnitObject(value, tokenPath);
    return `${value.value}${value.unit}`;
  }
 
  // If it's already a string, return as-is (for backward compatibility during transition)
  if (typeof value === 'string') {
    return value;
  }
 
  // Anything else is an error
  throw new Error(
    `valueUnitToString: Expected string or value+unit object${pathInfo}, got ${typeof value}: ${JSON.stringify(value)}`,
  );
}
 
/**
 * Parse a value+unit string into value and unit
 * Converts string format to DTCG-compliant object format
 * @param {string} str - The value+unit string (e.g., "0.25rem", "16px")
 * @returns {object|null} - Object with {value, unit} or null if not a value+unit string
 */
export function parseValueUnitString(str) {
  if (typeof str !== 'string') {
    return null;
  }
 
  // Match number (including decimals) followed by unit
  // Pattern: digits + optional (. + digits) + unit
  const match = str.match(new RegExp(`^(\\d+(\\.\\d+)?)(${SUPPORTED_UNITS_REGEX})$`));
 
  if (!match) {
    return null;
  }
 
  const value = parseFloat(match[1]);
  const unit = match[3]; // Capture group 3 due to nested group
 
  Iif (isNaN(value)) {
    return null;
  }
 
  return { value, unit };
}
 
/**
 * Get the raw value+unit value from a token, handling both formats
 * @param {object} token - The token object
 * @returns {string|object|null} - The value+unit value, or null if token is invalid
 */
export function getRawValueUnitValue(token) {
  if (!token) return null;
  return token.value || token.$value || token.original?.$value;
}
 
/**
 * Extract numeric value from value+unit token (for calculations)
 * @param {string|object} value - The value+unit value
 * @param {number|null} defaultValue - Value to return if extraction fails (default: null)
 * @returns {number|null} - The numeric value or defaultValue
 */
export function extractNumericValue(value, defaultValue = null) {
  if (isValueUnitObject(value)) {
    return value.value;
  }
 
  const parsed = parseValueUnitString(value);
  return parsed ? parsed.value : defaultValue;
}
 
/**
 * Extract unit from value+unit token
 * @param {string|object} value - The value+unit value
 * @param {string|null} defaultValue - Value to return if extraction fails (default: null)
 * @returns {string|null} - The unit or defaultValue
 */
export function extractUnit(value, defaultValue = null) {
  if (isValueUnitObject(value)) {
    return value.unit;
  }
 
  const parsed = parseValueUnitString(value);
  return parsed ? parsed.unit : defaultValue;
}