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

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                                          10x 10x                 319x                                   159x   159x 4x         155x 1x         154x 1x         153x 3x         150x 3x         147x 12x                               175x     175x 2x     173x   143x 143x       30x 25x       5x                       36x 5x         31x   31x 8x     23x 23x   23x       23x                 316x 312x                   8x 2x     6x 6x                   81x 65x     16x 16x    
/**
 * 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;
}