UNPKG

style-dictionary

Version:

Style once, use everywhere. A build system for creating cross-platform styles.

1,756 lines (1,632 loc) 57.8 kB
import Color from 'tinycolor2'; import ColorJS from 'colorjs.io'; import { camelCase, kebabCase, snakeCase } from 'change-case'; import { join } from 'path-unified/posix'; import { transformTypes, transforms } from '../enums/index.js'; import convertToBase64 from '../utils/convertToBase64.js'; import GroupMessages from '../utils/groupMessages.js'; /** * @typedef {import('../../types/Transform.d.ts').Transform} Transform * @typedef {import('../../types/DesignToken.d.ts').TransformedToken} Token * @typedef {import('../../types/DesignToken.d.ts').DTCGColorValue} DTCGColorValue * @typedef {import('../../types/DesignToken.d.ts').DTCGColorSpace} DTCGColorSpace * @typedef {import('../../types/Config.d.ts').PlatformConfig} PlatformConfig * @typedef {import('../../types/Config.d.ts').Config} Config * @typedef {import('../../types/TokenTypes.d.ts').TokenTypeDimension} TokenTypeDimension * @typedef {import('../../types/TokenTypes.d.ts').TokenTypeDimensionUnit} TokenTypeDimensionUnit * */ const UNKNOWN_CSS_FONT_PROPS_WARNINGS = GroupMessages.GROUP.UnknownCSSFontProperties; const { value, name, attribute } = transformTypes; /** * Valid DTCG v2025.10 color spaces * @type {Set<string>} */ const DTCG_COLOR_SPACES = new Set([ 'srgb', 'srgb-linear', 'display-p3', 'a98-rgb', 'prophoto-rgb', 'rec2020', 'xyz-d50', 'xyz-d65', 'lab', 'lch', 'oklab', 'oklch', 'hsl', 'hwb', ]); /** * Map DTCG color space names to colorjs.io color space IDs * @type {Record<string, string>} */ const DTCG_TO_COLORJS_SPACE = { srgb: 'srgb', 'srgb-linear': 'srgb-linear', 'display-p3': 'p3', 'a98-rgb': 'a98rgb', 'prophoto-rgb': 'prophoto', rec2020: 'rec2020', 'xyz-d50': 'xyz-d50', 'xyz-d65': 'xyz-d65', lab: 'lab', lch: 'lch', oklab: 'oklab', oklch: 'oklch', hsl: 'hsl', hwb: 'hwb', }; /** * Check if a value is a DTCG color object * @param {unknown} val * @returns {val is DTCGColorValue} */ export function isDTCGColorObject(val) { return ( typeof val === 'object' && val !== null && 'colorSpace' in val && typeof val.colorSpace === 'string' && DTCG_COLOR_SPACES.has(val.colorSpace) && 'components' in val && Array.isArray(val.components) ); } /** * Convert a DTCG color object to a ColorJS Color instance * Handles 'none' values in components and alpha * @param {DTCGColorValue} dtcgColor * @returns {ColorJS} */ function dtcgToColorJS(dtcgColor) { const { colorSpace, components, alpha } = dtcgColor; // Map DTCG color space to colorjs.io color space ID const colorjsSpace = DTCG_TO_COLORJS_SPACE[colorSpace] ?? colorSpace; // Convert 'none' values to NaN (as per CSS Color spec) const coords = /** @type {[number, number, number]} */ ( components.map((c) => (c === 'none' ? NaN : c)) ); const alphaValue = alpha === 'none' ? NaN : (alpha ?? 1); return new ColorJS(colorjsSpace, coords, alphaValue); } /** * Get the sRGB representation of a color, with gamut mapping if needed * Uses the hex property as fallback if provided and color is out of gamut * @param {DTCGColorValue} dtcgColor * @returns {{ r: number, g: number, b: number, a: number }} */ function dtcgColorToRgb(dtcgColor) { const color = dtcgToColorJS(dtcgColor); // Check if color is in sRGB gamut if (!color.inGamut('srgb')) { // If hex fallback is provided, use it if (dtcgColor.hex) { const fallback = Color(dtcgColor.hex).toRgb(); return fallback; } // Otherwise, perform gamut mapping color.toGamut({ space: 'srgb' }); } const srgb = color.to('srgb'); const alpha = isNaN(srgb.alpha) ? 1 : srgb.alpha; return { r: Math.round(Math.max(0, Math.min(1, srgb.coords[0])) * 255), g: Math.round(Math.max(0, Math.min(1, srgb.coords[1])) * 255), b: Math.round(Math.max(0, Math.min(1, srgb.coords[2])) * 255), a: alpha, }; } /** * Get a CSS compatible color string from a token value that contains a color property * Used in CSS shorthand transforms for token types that can have color poperties, like border, gradient, shadow. * @param {unknown} tokenValue * @returns {string | undefined} */ export function getColorStringFromTokenObjectValue(tokenValue) { /** @type {string | undefined} */ let hex; if (typeof tokenValue === 'object' && tokenValue !== null && 'color' in tokenValue) { if (typeof tokenValue.color === 'string') { return tokenValue.color; } let color = /** @type {DTCGColorValue} */ (tokenValue.color); let colorJS = getColorJS(color); hex = colorJS.toString(); } return hex; } /** * Get a tinycolor2 Color instance from either legacy string format or DTCG object format * This provides backward compatibility for transformers that use tinycolor2 methods * @param {Token} token * @param {Config} options * @returns {ReturnType<typeof Color>} */ export function getColor(token, options) { const val = options.usesDtcg ? token.$value : token.value; if (isDTCGColorObject(val)) { const rgb = dtcgColorToRgb(val); return Color(rgb); } return Color(val); } /** * Get a ColorJS Color instance from either legacy string format or DTCG object format * @param {DTCGColorValue | string} val * @returns {ColorJS} */ export function getColorJS(val) { if (isDTCGColorObject(val)) { return dtcgToColorJS(val); } // Parse legacy format with ColorJS return new ColorJS(val); } /** * @param {string} str * @returns {string} */ const UNICODE_PATTERN = /&#x([^;]+);/g; const camelOpts = { mergeAmbiguousCharacters: true, }; /** * @param {Token} token * @param {Config} options * @returns {boolean} */ export function isColor(token, options) { const val = options.usesDtcg ? token.$value : token.value; const type = options.usesDtcg ? token.$type : token.type; if (type !== 'color') return false; // DTCG object format if (isDTCGColorObject(val)) return true; // Legacy string format return ( Color(val).isValid() && // exclude gradients from being color transformed ['linear', 'radial', 'conic'] .map((pre) => `${pre}-gradient`) .every((pre) => !`${val}`.startsWith(pre) && !`${val}`.startsWith(`repeating-${pre}`)) ); } /** * @param {Token} token * @param {Config} options * @returns {boolean} */ function isDimension(token, options) { return (options.usesDtcg ? token.$type : token.type) === 'dimension'; } /** * @param {Token} token * @param {Config} options * @returns {boolean} */ function isFontSize(token, options) { return (options.usesDtcg ? token.$type : token.type) === 'fontSize'; } /** * @param {Token} token * @param {Config} options * @returns {boolean} */ function isAsset(token, options) { return (options.usesDtcg ? token.$type : token.type) === 'asset'; } /** * @param {Token} token * @param {Config} options * @returns {boolean} */ function isContent(token, options) { return (options.usesDtcg ? token.$type : token.type) === 'content'; } /** * @param {string} character * @param {Token} token * @param {Config} options * @returns {string} */ function wrapValueWith(character, token, options) { return `${character}${options.usesDtcg ? token.$value : token.value}${character}`; } /** * @param {Token} token * @param {Config} options * @returns {string} */ function wrapValueWithDoubleQuote(token, options) { return wrapValueWith('"', token, options); } /** * @param {string} name * @param {string|number} value * @param {string} unitType * @returns {string} */ function throwSizeError(name, value, unitType) { throw new Error( `Invalid Number: '${name}: ${value}' is not a valid number, cannot transform to '${unitType}' \n`, ); } /** * @param {PlatformConfig} config * @returns {number} */ function getBasePxFontSize(config) { return (config && config.basePxFontSize) || 16; } /** * @param {string} fontString */ function quoteWrapWhitespacedFont(fontString) { let fontName = fontString.trim(); const isQuoted = (fontName.startsWith("'") && fontName.endsWith("'")) || (fontName.startsWith('"') && fontName.endsWith('"')); if (!isQuoted) { fontName = fontName.replace(/'/g, "\\'"); } const hasWhiteSpace = new RegExp('\\s+').test(fontName); return hasWhiteSpace && !isQuoted ? `'${fontName}'` : fontName; } /** * @param {Token} token * @param {Config} options */ function processFontFamily(token, options) { const val = options.usesDtcg ? token.$value : token.value; const type = options.usesDtcg ? token.$type : token.type; /** * @param {string|string[]} _family * @returns */ const processFamily = (_family) => { let family = _family; if (typeof family === 'string' && family.includes(',')) { family = family.split(',').map((part) => part.trim()); } if (Array.isArray(family)) { return family.map((part) => quoteWrapWhitespacedFont(part)).join(', '); } return quoteWrapWhitespacedFont(family); }; if (type === 'typography') { if (val.fontFamily) { return { ...val, fontFamily: processFamily(val.fontFamily), }; } return val; } return processFamily(val); } /** * @param {Token} token * @param {Config} options */ function transformCubicBezierCSS(token, options) { const val = options.usesDtcg ? token.$value : token.value; const type = options.usesDtcg ? token.$type : token.type; /** @param {number[]|string} easing */ const transformEasing = (easing) => { if (Array.isArray(easing)) { return `cubic-bezier(${easing.join(', ')})`; } return easing; }; if (type === 'transition') { if (val.timingFunction) { return { ...val, timingFunction: transformEasing(val.timingFunction), }; } return val; } return transformEasing(val); } /** * Parse and normalize the value of a dimension Token * as DTCG dimension value -> { value: 15, unit: 'em' } * * Note: the result could contain a unitless dimension * Historically in Style Dictionary, number tokens were defined as unitless dimension tokens * We can `deprecated` this and remove that in a breaking change, forcing users to change them to number tokens * @param {Token['value']} _val * * unit can be undefined for this utility method as well to support old dimension token values * which can be defined as unitless dimension tokens or numbers * @returns {{ value: string | number; unit: TokenTypeDimensionUnit | undefined }} */ export function getTokenDimensionValue(_val) { let val = _val; /** @type {TokenTypeDimensionUnit | undefined} */ let unit; const valueObj = typeof val === 'object' && !Array.isArray(val) && val !== null; // old string dimension value if (!valueObj) { // if it contains a unit -> split it into unit and val const unitMatch = `${val}`.match(/[^0-9.-]+$/); // try to get the value already without unit const valWithoutUnit = `${val}`.replace(unitMatch?.[0] ?? '', ''); // guard that we can actually parse the value as a number if (unitMatch && !Number.isNaN(parseFloat(valWithoutUnit))) { unit = /** @type {TokenTypeDimensionUnit} */ (unitMatch[0]); val = valWithoutUnit; } } return valueObj ? { value: val.value, unit: val.unit } : { value: val, unit: unit ? unit : undefined }; } /** * @param {string | undefined} prop * @returns */ function normalizeDimensionProp(prop) { if (prop !== undefined) { const { value, unit } = getTokenDimensionValue(prop); return `${value}${unit ?? ''}`; } return prop; } /** * @namespace Transforms * @type {Record<string, Omit<Transform, 'name'>>} */ export default { /** * Adds: category, type, item, subitem, and state on the attributes object based on the location in the style dictionary. * * ```js * // Matches: all * // Returns: * { * "category": "color", * "type": "background", * "item": "button", * "subitem": "primary", * "state": "active" * } * ``` * * @memberof Transforms */ [transforms.attributeCti]: { type: attribute, transform: function (token) { const attrNames = ['category', 'type', 'item', 'subitem', 'state']; const originalAttrs = token.attributes || {}; /** @type {Record<string, string>} */ const generatedAttrs = {}; for (let i = 0; i < token.path.length && i < attrNames.length; i++) { generatedAttrs[attrNames[i]] = token.path[i]; } return Object.assign(generatedAttrs, originalAttrs); }, }, /** * Adds: hex, hsl, hsv, rgb, red, blue, green. * * ```js * // Matches: token.type === 'color' * // Returns * { * "hex": "009688", * "rgb": {"r": 0, "g": 150, "b": 136, "a": 1}, * "hsl": {"h": 174.4, "s": 1, "l": 0.294, "a": 1}, * "hsv": {"h": 174.4, "s": 1, "l": 0.588, "a": 1}, * } * ``` * * @memberof Transforms */ [transforms.attributeColor]: { type: attribute, filter: isColor, transform: function (token, _, options) { const color = getColor(token, options); return { hex: color.toHex(), rgb: color.toRgb(), hsl: color.toHsl(), hsv: color.toHsv(), }; }, }, /** * Creates a human-friendly name * * ```js * // Matches: All * // Returns: * "button primary" * ``` * * @memberof Transforms */ [transforms.nameHuman]: { type: name, transform: function (token) { return [token.attributes?.item, token.attributes?.subitem].join(' '); }, }, /** * Creates a camel case name. If you define a prefix on the platform in your config, it will prepend with your prefix * * ```js * // Matches: all * // Returns: * "colorBackgroundButtonPrimaryActive" * "prefixColorBackgroundButtonPrimaryActive" * ``` * * @memberof Transforms */ [transforms.nameCamel]: { type: name, transform: function (token, config) { return camelCase([config.prefix].concat(token.path).join(' '), camelOpts); }, }, /** * Creates a kebab case name. If you define a prefix on the platform in your config, it will prepend with your prefix * * ```js * // Matches: all * // Returns: * "color-background-button-primary-active" * "prefix-color-background-button-primary-active" * ``` * * @memberof Transforms */ [transforms.nameKebab]: { type: name, transform: function (token, config) { return kebabCase([config.prefix].concat(token.path).join(' ')); }, }, /** * Creates a snake case name. If you define a prefix on the platform in your config, it will prepend with your prefix * * ```js * // Matches: all * // Returns: * "color_background_button_primary_active" * "prefix_color_background_button_primary_active" * ``` * * @memberof Transforms */ [transforms.nameSnake]: { type: name, transform: function (token, config) { return snakeCase([config.prefix].concat(token.path).join(' ')); }, }, /** * Creates a constant-style name based on the full CTI of the token. If you define a prefix on the platform in your config, it will prepend with your prefix * * ```js * // Matches: all * // Returns: * "COLOR_BACKGROUND_BUTTON_PRIMARY_ACTIVE" * "PREFIX_COLOR_BACKGROUND_BUTTON_PRIMARY_ACTIVE" * ``` * * @memberof Transforms */ [transforms.nameConstant]: { type: name, transform: function (token, config) { return snakeCase([config.prefix].concat(token.path).join(' ')).toUpperCase(); }, }, /** * Creates a Pascal case name. If you define a prefix on the platform in your config, it will prepend with your prefix * * ```js * // Matches: all * // Returns: * "ColorBackgroundButtonPrimaryActive" * "PrefixColorBackgroundButtonPrimaryActive" * ``` * * @memberof Transforms */ [transforms.namePascal]: { type: name, transform: function (token, config) { /** @param {string} str */ const upperFirst = function (str) { return str ? str[0].toUpperCase() + str.slice(1) : ''; }; return upperFirst(camelCase([config.prefix].concat(token.path).join(' '), camelOpts)); }, }, /** * Transforms the value into an RGB string * * ```js * // Matches: token.type === 'color' * // Returns: * "rgb(0, 150, 136)" * ``` * * @memberof Transforms */ [transforms.colorRgb]: { type: value, filter: isColor, transform: function (token, _, options) { return getColor(token, options).toRgbString(); }, }, /** * Transforms the value into an HSL string or HSLA if alpha is present. Better browser support than color/hsl-4 * * ```js * // Matches: token.type === 'color' * // Returns: * "hsl(174, 100%, 29%)" * "hsl(174, 100%, 29%, .5)" * ``` * * @memberof Transforms */ [transforms.colorHsl]: { type: value, filter: isColor, transform: function (token, _, options) { return getColor(token, options).toHslString(); }, }, /** * Transforms the value into an HSL string, using fourth argument if alpha is present. * * ```js * // Matches: token.type === 'color' * // Returns: * "hsl(174 100% 29%)" * "hsl(174 100% 29% / .5)" * ``` * * @memberof Transforms */ [transforms.colorHsl4]: { type: value, filter: isColor, transform: function (token, _, options) { const color = getColor(token, options); const o = color.toHsl(); const vals = `${Math.round(o.h)} ${Math.round(o.s * 100)}% ${Math.round(o.l * 100)}%`; if (color.getAlpha() === 1) { return `hsl(${vals})`; } else { return `hsl(${vals} / ${o.a})`; } }, }, /** * Transforms the value into a 6-digit (if alpha is undefined or 1.0) or 8-digit hex string * * ```js * // Matches: token.type === 'color' * // Returns: * "#009688DE" * ``` * * @memberof Transforms */ [transforms.colorHex]: { type: value, filter: isColor, transform: function (token, _, options) { const color = getColor(token, options); if (color.getAlpha() === 1) { return color.toHexString(); } else { return color.toHex8String(); } }, }, /** * Transforms the value into an 8-digit hex string * * ```js * // Matches: token.type === 'color' * // Returns: * "#009688ff" * ``` * @deprecated use colorHex ('color/hex') instead, which supports hex8 now. * @memberof Transforms */ [transforms.colorHex8]: { type: value, filter: isColor, transform: function (token, _, options) { return getColor(token, options).toHex8String(); }, }, /** * Transforms the value into an 8-digit hex string for Android because they put the alpha channel first * * ```js * // Matches: token.type === 'color' * // Returns: * "#ff009688" * ``` * * @memberof Transforms */ [transforms.colorHex8android]: { type: value, filter: isColor, transform: function (token, _, options) { const str = getColor(token, options).toHex8(); return '#' + str.slice(6) + str.slice(0, 6); }, }, /** * Transforms the value into a Color class for Compose * * ```kotlin * // Matches: token.type === 'color' * // Returns: * Color(0xFF009688) * ``` * * @memberof Transforms */ [transforms.colorComposeColor]: { type: value, filter: isColor, transform: function (token, _, options) { const str = getColor(token, options).toHex8(); return 'Color(0x' + str.slice(6) + str.slice(0, 6) + ')'; }, }, /** * Transforms the value into an UIColor class for iOS * * ```objective-c * // Matches: token.type === 'color' * // Returns: * [UIColor colorWithRed:0.114f green:0.114f blue:0.114f alpha:1.000f] * ``` * * @memberof Transforms */ [transforms.colorUIColor]: { type: value, filter: isColor, transform: function (token, _, options) { const rgb = getColor(token, options).toRgb(); return ( '[UIColor colorWithRed:' + (rgb.r / 255).toFixed(3) + 'f' + ' green:' + (rgb.g / 255).toFixed(3) + 'f' + ' blue:' + (rgb.b / 255).toFixed(3) + 'f' + ' alpha:' + rgb.a.toFixed(3) + 'f]' ); }, }, /** * Transforms the value into an UIColor swift class for iOS * * ```swift * // Matches: token.type === 'color' * // Returns: * UIColor(red: 0.667, green: 0.667, blue: 0.667, alpha: 0.6) * ``` * * @memberof Transforms */ [transforms.colorUIColorSwift]: { type: value, filter: isColor, transform: function (token, _, options) { const { r, g, b, a } = getColor(token, options).toRgb(); const rFixed = (r / 255.0).toFixed(3); const gFixed = (g / 255.0).toFixed(3); const bFixed = (b / 255.0).toFixed(3); return `UIColor(red: ${rFixed}, green: ${gFixed}, blue: ${bFixed}, alpha: ${a})`; }, }, /** * Transforms the value into an UIColor swift class for iOS * * ```swift * // Matches: token.type === 'color' * // Returns: * Color(red: 0.667, green: 0.667, blue: 0.667, opacity: 0.6) * ``` * * @memberof Transforms */ [transforms.colorColorSwiftUI]: { type: value, filter: isColor, transform: function (token, _, options) { const { r, g, b, a } = getColor(token, options).toRgb(); const rFixed = (r / 255.0).toFixed(3); const gFixed = (g / 255.0).toFixed(3); const bFixed = (b / 255.0).toFixed(3); return `Color(red: ${rFixed}, green: ${gFixed}, blue: ${bFixed}, opacity: ${a})`; }, }, /** * Transforms the value into a hex or rgb string depending on if it has transparency * * ```css * // Matches: token.type === 'color' * // Returns: * #000000 * rgba(0,0,0,0.5) * ``` * * @memberof Transforms */ [transforms.colorCss]: { type: value, filter: isColor, transform: function (token, _, options) { const color = getColor(token, options); if (color.getAlpha() === 1) { return color.toHexString(); } else { return color.toRgbString(); } }, }, /** * * Transforms a color into an object with red, green, blue, and alpha * attributes that are floats from 0 - 1. This object is how Sketch stores * colors. * * ```js * // Matches: token.type === 'color' * // Returns: * { * red: 0.5, * green: 0.5, * blue: 0.5, * alpha: 1 * } * ``` * @memberof Transforms */ [transforms.colorSketch]: { type: value, filter: isColor, transform: function (token, _, options) { let color = getColor(token, options).toRgb(); return { red: (color.r / 255).toFixed(5), green: (color.g / 255).toFixed(5), blue: (color.b / 255).toFixed(5), alpha: color.a, }; }, }, /** * Transforms the value into an OKLCH CSS color function string. * Preserves wide-gamut colors without lossy gamut mapping. * * ```css * // Matches: token.type === 'color' * // Returns: * "oklch(0.7 0.15 180)" * "oklch(0.7 0.15 180 / 0.5)" * ``` * * @memberof Transforms */ [transforms.colorOklch]: { type: value, filter: isColor, transform: function (token, _, options) { const val = options.usesDtcg ? token.$value : token.value; const color = getColorJS(val).to('oklch'); const [l, c, h] = color.coords; const alpha = isNaN(color.alpha) ? 1 : color.alpha; // Format: oklch(L C H) or oklch(L C H / alpha) const lStr = (isNaN(l) ? 0 : l).toFixed(4); const cStr = (isNaN(c) ? 0 : c).toFixed(4); const hStr = (isNaN(h) ? 0 : h).toFixed(2); if (alpha === 1) { return `oklch(${lStr} ${cStr} ${hStr})`; } return `oklch(${lStr} ${cStr} ${hStr} / ${alpha})`; }, }, /** * Transforms the value into an OKLAB CSS color function string. * Preserves wide-gamut colors without lossy gamut mapping. * * ```css * // Matches: token.type === 'color' * // Returns: * "oklab(0.7 -0.1 0.1)" * "oklab(0.7 -0.1 0.1 / 0.5)" * ``` * * @memberof Transforms */ [transforms.colorOklab]: { type: value, filter: isColor, transform: function (token, _, options) { const val = options.usesDtcg ? token.$value : token.value; const color = getColorJS(val).to('oklab'); const [l, a, b] = color.coords; const alpha = isNaN(color.alpha) ? 1 : color.alpha; // Format: oklab(L a b) or oklab(L a b / alpha) const lStr = (isNaN(l) ? 0 : l).toFixed(4); const aStr = (isNaN(a) ? 0 : a).toFixed(4); const bStr = (isNaN(b) ? 0 : b).toFixed(4); if (alpha === 1) { return `oklab(${lStr} ${aStr} ${bStr})`; } return `oklab(${lStr} ${aStr} ${bStr} / ${alpha})`; }, }, /** * Transforms the value into a Display P3 CSS color function string. * Preserves wide-gamut colors without lossy gamut mapping. * * ```css * // Matches: token.type === 'color' * // Returns: * "color(display-p3 0.5 0.5 0.5)" * "color(display-p3 0.5 0.5 0.5 / 0.5)" * ``` * * @memberof Transforms */ [transforms.colorP3]: { type: value, filter: isColor, transform: function (token, _, options) { const val = options.usesDtcg ? token.$value : token.value; const color = getColorJS(val).to('p3'); const [r, g, b] = color.coords; const alpha = isNaN(color.alpha) ? 1 : color.alpha; // Format: color(display-p3 r g b) or color(display-p3 r g b / alpha) const rStr = (isNaN(r) ? 0 : r).toFixed(5); const gStr = (isNaN(g) ? 0 : g).toFixed(5); const bStr = (isNaN(b) ? 0 : b).toFixed(5); if (alpha === 1) { return `color(display-p3 ${rStr} ${gStr} ${bStr})`; } return `color(display-p3 ${rStr} ${gStr} ${bStr} / ${alpha})`; }, }, /** * Transforms the value into an LCH CSS color function string. * Preserves wide-gamut colors without lossy gamut mapping. * * ```css * // Matches: token.type === 'color' * // Returns: * "lch(50% 30 180)" * "lch(50% 30 180 / 0.5)" * ``` * * @memberof Transforms */ [transforms.colorLch]: { type: value, filter: isColor, transform: function (token, _, options) { const val = options.usesDtcg ? token.$value : token.value; const color = getColorJS(val).to('lch'); const [l, c, h] = color.coords; const alpha = isNaN(color.alpha) ? 1 : color.alpha; // Format: lch(L% C H) or lch(L% C H / alpha) const lStr = (isNaN(l) ? 0 : l).toFixed(2); const cStr = (isNaN(c) ? 0 : c).toFixed(2); const hStr = (isNaN(h) ? 0 : h).toFixed(2); if (alpha === 1) { return `lch(${lStr}% ${cStr} ${hStr})`; } return `lch(${lStr}% ${cStr} ${hStr} / ${alpha})`; }, }, /** * Transforms the value into a scale-independent pixel (sp) value for font sizes on Android. It will not scale the number. * * ```js * // Matches: token.type === 'fontSize' * // Returns: * "10.00sp" * ``` * * @memberof Transforms */ [transforms.sizeSp]: { type: value, filter: isFontSize, transform: function (token, _, options) { const tokenVal = options.usesDtcg ? token.$value : token.value; const nonParsed = getTokenDimensionValue(tokenVal); const val = parseFloat(`${nonParsed.value}`); if (isNaN(val)) throwSizeError(token.name, options.usesDtcg ? token.$value : token.value, 'sp'); return `${val.toFixed(2)}sp`; }, }, /** * Transforms the value into a density-independent pixel (dp) value for non-font sizes on Android. It will not scale the number. * * ```js * // Matches: token.type === 'dimension' * // Returns: * "10.00dp" * ``` * * @memberof Transforms */ [transforms.sizeDp]: { type: value, filter: isDimension, transform: function (token, _, options) { const tokenVal = options.usesDtcg ? token.$value : token.value; const nonParsed = getTokenDimensionValue(tokenVal); const val = parseFloat(`${nonParsed.value}`); if (isNaN(val)) throwSizeError(token.name, options.usesDtcg ? token.$value : token.value, 'dp'); return `${val.toFixed(2)}dp`; }, }, /** * Transforms the value into a useful object ( for React Native support ) * * ```js * // Matches: token.type === 'dimension' * // Returns: * { * original: "10px", * number: 10, * decimal: 0.1, // 10 divided by 100 * scale: 160, // 10 times 16 * } * ``` * * @memberof Transforms */ [transforms.sizeObject]: { type: value, filter: (token, options) => isDimension(token, options) || isFontSize(token, options), transform: function (token, config, options) { const tokenVal = options.usesDtcg ? token.$value : token.value; const nonParsed = getTokenDimensionValue(tokenVal); const parsedVal = parseFloat(`${nonParsed.value}`); if (isNaN(parsedVal)) throwSizeError(token.name, options.usesDtcg ? token.$value : token.value, 'object'); return { original: options.usesDtcg ? token.$value : token.value, number: parsedVal, decimal: parsedVal / 100, scale: parsedVal * getBasePxFontSize(config), }; }, }, /** * Transforms the value from a REM size on web into a scale-independent pixel (sp) value for font sizes on Android. It WILL scale the number by a factor of 16 (or the value of 'basePxFontSize' on the platform in your config). * * ```js * // Matches: token.type === 'fontSize' * // Returns: * "16.00sp" * ``` * * @memberof Transforms */ [transforms.sizeRemToSp]: { type: value, filter: isFontSize, transform: function (token, config, options) { const tokenVal = options.usesDtcg ? token.$value : token.value; const nonParsed = getTokenDimensionValue(tokenVal); const parsedVal = parseFloat(`${nonParsed.value}`); const baseFont = getBasePxFontSize(config); if (isNaN(parsedVal)) throwSizeError(token.name, options.usesDtcg ? token.$value : token.value, 'sp'); return `${(parsedVal * baseFont).toFixed(2)}sp`; }, }, /** * Transforms the value from a REM size on web into a density-independent pixel (dp) value for non font-sizes on Android. * It WILL scale the number by a factor of 16 (or the value of 'basePxFontSize' on the platform in your config). * * ```js * // Matches: token.type === 'dimension' * // Returns: * "16.00dp" * ``` * * @memberof Transforms */ [transforms.sizeRemToDp]: { type: value, filter: isDimension, transform: function (token, config, options) { const tokenVal = options.usesDtcg ? token.$value : token.value; const nonParsed = getTokenDimensionValue(tokenVal); const parsedVal = parseFloat(`${nonParsed.value}`); const baseFont = getBasePxFontSize(config); if (isNaN(parsedVal)) throwSizeError(token.name, options.usesDtcg ? token.$value : token.value, 'dp'); return `${(parsedVal * baseFont).toFixed(2)}dp`; }, }, /** * Adds 'px' to the end of the number. Does not scale the number * * ```js * // Matches: token.type === 'dimension' * // Returns: * "10px" * ``` * * @memberof Transforms */ [transforms.sizePx]: { type: value, filter: (token, options) => isDimension(token, options) || isFontSize(token, options), transform: function (token, _, options) { const tokenVal = options.usesDtcg ? token.$value : token.value; const nonParsed = getTokenDimensionValue(tokenVal); const parsedVal = parseFloat(`${nonParsed.value}`); if (isNaN(parsedVal)) throwSizeError(token.name, options.usesDtcg ? token.$value : token.value, 'px'); return `${parsedVal}px`; }, }, /** * Adds 'rem' to the end of the number. Does not scale the number * * ```js * // Matches: token.type === 'dimension' * // Returns: * "10rem" * ``` * * @memberof Transforms */ [transforms.sizeRem]: { type: value, filter: (token, options) => isDimension(token, options) || isFontSize(token, options), transform: function (token, _, options) { const tokenVal = options.usesDtcg ? token.$value : token.value; const nonParsed = getTokenDimensionValue(tokenVal); const parsedVal = parseFloat(`${nonParsed.value}`); if (isNaN(parsedVal)) throwSizeError(token.name, options.usesDtcg ? token.$value : token.value, 'rem'); // Preserve the unit when one was explicitly provided. Must run before the // unitless-zero short-circuit below, otherwise a value like `"0em"` loses // its unit and falls through to the number `0` branch. if (nonParsed.unit !== undefined) { return `${nonParsed.value}${nonParsed.unit}`; } if (parsedVal === 0) return nonParsed.value; return `${parsedVal}rem`; }, }, /** * Scales the number by 16 (or the value of 'basePxFontSize' on the platform in your config) and adds 'f' to the end. * * ```js * // Matches: token.type === 'dimension' * // Returns: * "16.00f" * ``` * * @memberof Transforms */ [transforms.sizeRemToFloat]: { type: value, filter: (token, options) => isDimension(token, options) || isFontSize(token, options), transform: function (token, config, options) { const tokenVal = options.usesDtcg ? token.$value : token.value; const nonParsed = getTokenDimensionValue(tokenVal); const parsedVal = parseFloat(`${nonParsed.value}`); const baseFont = getBasePxFontSize(config); if (isNaN(parsedVal)) throwSizeError(token.name, options.usesDtcg ? token.$value : token.value, 'f'); return `${(parsedVal * baseFont).toFixed(2)}f`; }, }, /** * Scales the number by 16 (or the value of 'basePxFontSize' on the platform in your config) and adds 'f' to the end. * * ```js * // Matches: token.type === 'dimension' * // Returns: * "16.00pt" * ``` * * @memberof Transforms */ [transforms.sizeRemToPt]: { type: value, filter: (token, options) => isDimension(token, options) || isFontSize(token, options), transform: function (token, config, options) { const tokenVal = options.usesDtcg ? token.$value : token.value; const nonParsed = getTokenDimensionValue(tokenVal); const parsedVal = parseFloat(`${nonParsed.value}`); const baseFont = getBasePxFontSize(config); if (isNaN(parsedVal)) throwSizeError(token.name, options.usesDtcg ? token.$value : token.value, 'pt'); return `${(parsedVal * baseFont).toFixed(2)}pt`; }, }, /** * Transforms the value from a REM size on web into a scale-independent pixel (sp) value for font sizes in Compose. * It WILL scale the number by a factor of 16 (or the value of 'basePxFontSize' on the platform in your config). * * ```kotlin * // Matches: token.type === 'fontSize' * // Returns: * "16.00.sp" * ``` * * @memberof Transforms */ [transforms.sizeComposeRemToSp]: { type: value, filter: isFontSize, transform: function (token, config, options) { const tokenVal = options.usesDtcg ? token.$value : token.value; const nonParsed = getTokenDimensionValue(tokenVal); const parsedVal = parseFloat(`${nonParsed.value}`); const baseFont = getBasePxFontSize(config); if (isNaN(parsedVal)) throwSizeError(token.name, options.usesDtcg ? token.$value : token.value, 'sp'); return `${(parsedVal * baseFont).toFixed(2)}.sp`; }, }, /** * Transforms the value from a REM size on web into a density-independent pixel (dp) value for font sizes in Compose. It WILL scale the number by a factor of 16 (or the value of 'basePxFontSize' on the platform in your config). * * ```kotlin * // Matches: token.type === 'dimension' * // Returns: * "16.00.dp" * ``` * * @memberof Transforms */ [transforms.sizeComposeRemToDp]: { type: value, filter: isDimension, transform: function (token, config, options) { const tokenVal = options.usesDtcg ? token.$value : token.value; const nonParsed = getTokenDimensionValue(tokenVal); const parsedVal = parseFloat(`${nonParsed.value}`); const baseFont = getBasePxFontSize(config); if (isNaN(parsedVal)) throwSizeError(token.name, options.usesDtcg ? token.$value : token.value, 'dp'); return `${(parsedVal * baseFont).toFixed(2)}.dp`; }, }, /** * Adds the .em Compose extension to the end of a number. Does not scale the value * * ```kotlin * // Matches: token.type === 'fontSize' * // Returns: * "16.em" * ``` * * @memberof Transforms */ [transforms.sizeComposeEm]: { type: value, filter: isFontSize, transform: function (token, _, options) { const tokenVal = options.usesDtcg ? token.$value : token.value; const nonParsed = getTokenDimensionValue(tokenVal); const parsedVal = parseFloat(`${nonParsed.value}`); if (isNaN(parsedVal)) throwSizeError(token.name, options.usesDtcg ? token.$value : token.value, 'em'); return `${parsedVal}.em`; }, }, /** * Adds the .sp Compose extension to the end of a number. Does not scale the value * * ```kotlin * // Matches: token.type === 'fontSize' * // Returns: * "16.00.sp" * ``` * * @memberof Transforms */ [transforms.sizeComposeSp]: { type: value, filter: isFontSize, transform: function (token, _, options) { const tokenVal = options.usesDtcg ? token.$value : token.value; const nonParsed = getTokenDimensionValue(tokenVal); const val = parseFloat(`${nonParsed.value}`); if (isNaN(val)) throwSizeError(token.name, options.usesDtcg ? token.$value : token.value, 'sp'); return `${val.toFixed(2)}.sp`; }, }, /** * Adds the .dp Compose extension to the end of a number. Does not scale the value * * ```kotlin * // Matches: token.type === 'dimension' * // Returns: * "16.00.dp" * ``` * * @memberof Transforms */ [transforms.sizeComposeDp]: { type: value, filter: isDimension, transform: function (token, _, options) { const tokenVal = options.usesDtcg ? token.$value : token.value; const nonParsed = getTokenDimensionValue(tokenVal); const val = parseFloat(`${nonParsed.value}`); if (isNaN(val)) throwSizeError(token.name, options.usesDtcg ? token.$value : token.value, 'dp'); return `${val.toFixed(2)}.dp`; }, }, /** * Scales the number by 16 (or the value of 'basePxFontSize' on the platform in your config) to get to points for Swift and initializes a CGFloat * * ```js * // Matches: token.type === 'dimension' * // Returns: "CGFloat(16.00)"" * ``` * * @memberof Transforms */ [transforms.sizeSwiftRemToCGFloat]: { type: value, filter: (token, options) => isDimension(token, options) || isFontSize(token, options), transform: function (token, config, options) { const tokenVal = options.usesDtcg ? token.$value : token.value; const nonParsed = getTokenDimensionValue(tokenVal); const parsedVal = parseFloat(`${nonParsed.value}`); const baseFont = getBasePxFontSize(config); if (isNaN(parsedVal)) throwSizeError(token.name, options.usesDtcg ? token.$value : token.value, 'CGFloat'); return `CGFloat(${(parsedVal * baseFont).toFixed(2)})`; }, }, /** * Scales the number by 16 (or the value of 'basePxFontSize' on the platform in your config) and adds 'px' to the end. * * ```js * // Matches: token.type === 'dimension' * // Returns: * "16px" * ``` * * @memberof Transforms */ [transforms.sizeRemToPx]: { type: value, filter: (token, options) => isDimension(token, options) || isFontSize(token, options), transform: function (token, config, options) { const tokenVal = options.usesDtcg ? token.$value : token.value; const nonParsed = getTokenDimensionValue(tokenVal); const parsedVal = parseFloat(`${nonParsed.value}`); const baseFont = getBasePxFontSize(config); if (isNaN(parsedVal)) throwSizeError(token.name, options.usesDtcg ? token.$value : token.value, 'px'); return `${(parsedVal * baseFont).toFixed(0)}px`; }, }, /** * Scales non-zero numbers to rem, and adds 'rem' to the end. If you define a "basePxFontSize" on the platform in your config, * it will be used to scale the value, otherwise 16 (default web font size) will be used. * * ```js * // Matches: token.type === 'dimension' * // Returns: * 0 * "0" * "1rem" * ``` * * @memberof Transforms */ [transforms.sizePxToRem]: { type: value, filter: (token, options) => isDimension(token, options) || isFontSize(token, options), transform: (token, config, options) => { const tokenVal = options.usesDtcg ? token.$value : token.value; const nonParsed = getTokenDimensionValue(tokenVal); const parsedVal = parseFloat(`${nonParsed.value}`); const baseFont = getBasePxFontSize(config); if (isNaN(parsedVal)) throwSizeError(token.name, options.usesDtcg ? token.$value : token.value, 'rem'); if (parsedVal === 0) return nonParsed.value; return `${parsedVal / baseFont}rem`; }, }, /** * Scales the number by 16 (or the value of 'basePxFontSize' on the platform in your config) to get to points for Flutter * * ```dart * // Matches: token.type === 'dimension' * // Returns: 16.00 * ``` * * @memberof Transforms */ [transforms.sizeFlutterRemToDouble]: { type: value, filter: (token, options) => isDimension(token, options) || isFontSize(token, options), transform: function (token, config, options) { const tokenVal = options.usesDtcg ? token.$value : token.value; const nonParsed = getTokenDimensionValue(tokenVal); const baseFont = getBasePxFontSize(config); const parsedVal = parseFloat(`${nonParsed.value}`); if (isNaN(parsedVal)) throwSizeError(token.name, options.usesDtcg ? token.$value : token.value, 'Number'); return (parsedVal * baseFont).toFixed(2); }, }, /** * Takes a unicode point and transforms it into a form CSS can use. * * ```js * // Matches: token.type === 'html' * // Returns: * "'\\E001'" * ``` * * @memberof Transforms */ [transforms.htmlIcon]: { type: value, filter: function (token, options) { return (options.usesDtcg ? token.$type : token.type) === 'html'; }, transform: function (token, _, options) { return (options.usesDtcg ? token.$value : token.value).replace( UNICODE_PATTERN, /** * @param {string} _ * @param {string} variable */ function (_, variable) { return "'\\" + variable + "'"; }, ); }, }, /** * Wraps the value in a single quoted string * * ```js * // Matches: token.type === 'content' * // Returns: * "'string'" * ``` * * @memberof Transforms */ [transforms.contentQuote]: { type: value, filter: isContent, transform: function (token, _, options) { return wrapValueWith("'", token, options); }, }, /** * Wraps the value in a double-quoted string and prepends an '@' to make a string literal. * * ```objective-c * // Matches: token.type === 'content' * // Returns: * \@"string" * ``` * * @memberof Transforms */ [transforms.contentObjCLiteral]: { type: value, filter: isContent, transform: function (token, _, options) { return '@' + wrapValueWithDoubleQuote(token, options); }, }, /** * Wraps the value in a double-quoted string to make a string literal. * * ```swift * // Matches: token.type === 'content' * // Returns: * "string" * ``` * * @memberof Transforms */ [transforms.contentSwiftLiteral]: { type: value, filter: isContent, transform: (token, _, options) => wrapValueWithDoubleQuote(token, options), }, /** * Assumes a time in miliseconds and transforms it into a decimal * * ```js * // Matches: token.type === 'time' * // Returns: * "0.5s" * ``` * * @memberof Transforms */ [transforms.timeSeconds]: { type: value, filter: function (token, options) { return (options.usesDtcg ? token.$type : token.type) === 'time'; }, transform: function (token, _, options) { return (parseFloat(options.usesDtcg ? token.$value : token.value) / 1000).toFixed(2) + 's'; }, }, /** * Turns fontFamily tokens into valid CSS string values * https://design-tokens.github.io/community-group/format/#font-family * https://developer.mozilla.org/en-US/docs/Web/CSS/font-family * * ```js * // Matches: token.type === 'fontFamily' * // Returns: * "'Arial Narrow', Arial, sans-serif" * ```. * * @memberof Transforms */ [transforms.fontFamilyCss]: { type: value, // typography properties can be references, while fontFamily prop might not transitive: true, filter: (token, options) => { const type = options.usesDtcg ? token.$type : token.type; return !!type && ['fontFamily', 'typography'].includes(type); }, transform: (token, _, options) => { return processFontFamily(token, options); }, }, /** * Turns fontFamily tokens into valid CSS string values * https://design-tokens.github.io/community-group/format/#font-family * https://developer.mozilla.org/en-US/docs/Web/CSS/font-family * * ```js * // Matches: token.type === 'fontFamily' * // Returns: * "'Arial Narrow', Arial, sans-serif" * ```. * * @memberof Transforms */ [transforms.cubicBezierCss]: { type: value, // transition properties can be references, while timingFunction might not be transitive: true, filter: (token, options) => { const type = options.usesDtcg ? token.$type : token.type; return !!type && ['cubicBezier', 'transition'].includes(type); }, transform: (token, _, options) => { return transformCubicBezierCSS(token, options); }, }, /** * Turns strokeStyle object-value tokens into stringified CSS fallback * https://design-tokens.github.io/community-group/format/#stroke-style * https://design-tokens.github.io/community-group/format/#example-fallback-for-object-stroke-style * CSS does not allow detailed control of the dash pattern or line caps on dashed borders, so we use dashed fallback * ```js * // Matches: token.type === 'border' * // Returns: * "dashed" * ```. * * @memberof Transforms */ [transforms.strokeStyleCssShorthand]: { type: value, // border properties can be references, while style property might not be transitive: true, filter: (token, options) => (options.usesDtcg ? token.$type : token.type) === 'strokeStyle', transform: (token, _, options) => { const val = options.usesDtcg ? token.$value : token.value; if (typeof val !== 'object') { // already transformed to string return val; } return 'dashed'; }, }, /** * Turns border tokens object-value into stringified CSS shorthand * https://design-tokens.github.io/community-group/format/#border * * ```js * // Matches: token.type === 'border' * // Returns: * "2px solid #000000" * ```. * * @memberof Transforms */ [transforms.borderCssShorthand]: { type: value, // border properties can be references transitive: true, filter: (token, options) => (options.usesDtcg ? token.$type : token.type) === 'border', transform: (token, _, options) => { const val = options.usesDtcg ? token.$value : token.value; if (typeof val !== 'object') { // already transformed to string return val; } const colorStr = getColorStringFromTokenObjectValue(val); let { width } = val; // normalize for DTCG dimension object values width = normalizeDimensionProp(width); // use fallback for style object value, since CSS does not allow // detailed control of the dash pattern or line caps on dashed borders // https://design-tokens.github.io/community-group/format/#example-fallback-for-object-stroke-style // this means we also don't have to take care of dashArray containing dimension object values let { style } = val; if (typeof style === 'object') { style = 'dashed'; } return `${width ? `${width} ` : ''}${style ? `${style}` : 'none'}${colorStr ? ` ${colorStr}` : ''}`; }, }, /** * Turns typography tokens object-value into stringified CSS shorthand * https://design-tokens.github.io/community-group/format/#typography * * Available props within typography has been extended here * to include those available in CSS font shorthand: * https://developer.mozilla.org/en-US/docs/Web/CSS/font * * ```js * // Matches: token.type === 'typography' * // Returns: * "500 20px/1.5 Arial" * ```. * * @memberof Transforms */ [transforms.typographyCssShorthand]: { type: value, // typography properties can be references transitive: true, filter: (token, options) => (options.usesDtcg ? token.$type : token.type) === 'typography', transform: (token, platform, options) => { const val = optio