style-dictionary
Version:
Style once, use everywhere. A build system for creating cross-platform styles.
1,526 lines (1,432 loc) • 42.3 kB
JavaScript
/*
* Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with
* the License. A copy of the License is located at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
* CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions
* and limitations under the License.
*/
import Color from 'tinycolor2';
import { join } from 'path-unified';
import { snakeCase, kebabCase, camelCase } from 'change-case';
import convertToBase64 from '../utils/convertToBase64.js';
import GroupMessages from '../utils/groupMessages.js';
import { transforms, transformTypes } from '../enums/index.js';
/**
* @typedef {import('../../types/Transform.d.ts').Transform} Transform
* @typedef {import('../../types/DesignToken.d.ts').TransformedToken} Token
* @typedef {import('../../types/Config.d.ts').PlatformConfig} PlatformConfig
* @typedef {import('../../types/Config.d.ts').Config} Config
*/
const UNKNOWN_CSS_FONT_PROPS_WARNINGS = GroupMessages.GROUP.UnknownCSSFontProperties;
const { value, name, attribute } = transformTypes;
/**
* @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) {
return (
(options.usesDtcg ? token.$type : token.type) === 'color' &&
Color(options.usesDtcg ? token.$value : token.value).isValid()
);
}
/**
* @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 `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("'");
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);
}
/**
* @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 = Color(options.usesDtcg ? token.$value : token.value);
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 Color(options.usesDtcg ? token.$value : token.value).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 Color(options.usesDtcg ? token.$value : token.value).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 = Color(options.usesDtcg ? token.$value : token.value);
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 an 6-digit hex string
*
* ```js
* // Matches: token.type === 'color'
* // Returns:
* "#009688"
* ```
*
* @memberof Transforms
*/
[transforms.colorHex]: {
type: value,
filter: isColor,
transform: function (token, _, options) {
return Color(options.usesDtcg ? token.$value : token.value).toHexString();
},
},
/**
* Transforms the value into an 8-digit hex string
*
* ```js
* // Matches: token.type === 'color'
* // Returns:
* "#009688ff"
* ```
*
* @memberof Transforms
*/
[transforms.colorHex8]: {
type: value,
filter: isColor,
transform: function (token, _, options) {
return Color(options.usesDtcg ? token.$value : token.value).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 = Color(options.usesDtcg ? token.$value : token.value).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 = Color(options.usesDtcg ? token.$value : token.value).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 = Color(options.usesDtcg ? token.$value : token.value).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 } = Color(options.usesDtcg ? token.$value : token.value).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 } = Color(options.usesDtcg ? token.$value : token.value).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 = Color(options.usesDtcg ? token.$value : token.value);
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 = Color(options.usesDtcg ? token.$value : token.value).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 a scale-independent pixel (sp) value for font sizes on Android. It will not scale the number.
*
* ```js
* // Matches: token.type === 'fontSize'
* // Returns:
* "10.0sp"
* ```
*
* @memberof Transforms
*/
[transforms.sizeSp]: {
type: value,
filter: isFontSize,
transform: function (token, _, options) {
const nonParsedVal = options.usesDtcg ? token.$value : token.value;
const val = parseFloat(nonParsedVal);
if (isNaN(val)) throwSizeError(token.name, nonParsedVal, '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.0dp"
* ```
*
* @memberof Transforms
*/
[transforms.sizeDp]: {
type: value,
filter: isDimension,
transform: function (token, _, options) {
const nonParsedVal = options.usesDtcg ? token.$value : token.value;
const val = parseFloat(nonParsedVal);
if (isNaN(val)) throwSizeError(token.name, nonParsedVal, '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 value = options.usesDtcg ? token.$value : token.value;
const parsedVal = parseFloat(value);
if (isNaN(parsedVal)) throwSizeError(token.name, value, 'object');
return {
original: 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.0sp"
* ```
*
* @memberof Transforms
*/
[transforms.sizeRemToSp]: {
type: value,
filter: isFontSize,
transform: function (token, config, options) {
const value = options.usesDtcg ? token.$value : token.value;
const parsedVal = parseFloat(value);
const baseFont = getBasePxFontSize(config);
if (isNaN(parsedVal)) throwSizeError(token.name, 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.0dp"
* ```
*
* @memberof Transforms
*/
[transforms.sizeRemToDp]: {
type: value,
filter: isDimension,
transform: function (token, config, options) {
const value = options.usesDtcg ? token.$value : token.value;
const parsedVal = parseFloat(value);
const baseFont = getBasePxFontSize(config);
if (isNaN(parsedVal)) throwSizeError(token.name, 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 value = options.usesDtcg ? token.$value : token.value;
const parsedVal = parseFloat(value);
if (isNaN(parsedVal)) throwSizeError(token.name, 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 nonParsed = options.usesDtcg ? token.$value : token.value;
// if the dimension already has a unit (non-digit / . period character)
if (`${nonParsed}`.match(/[^0-9.-]+$/)) {
return nonParsed;
}
const parsedVal = parseFloat(nonParsed);
if (isNaN(parsedVal)) throwSizeError(token.name, nonParsed, 'rem');
if (parsedVal === 0) return Number.isInteger(nonParsed) ? 0 : '0';
return parsedVal + 'rem';
},
},
/**
* Scales the number by 16 (or the value of 'basePxFontSize' on the platform in your config) and adds 'pt' to the end.
*
* ```js
* // Matches: token.type === 'dimension'
* // Returns:
* "16pt"
* ```
*
* @memberof Transforms
*/
[transforms.sizeRemToPt]: {
type: value,
filter: (token, options) => isDimension(token, options) || isFontSize(token, options),
transform: function (token, config, options) {
const value = options.usesDtcg ? token.$value : token.value;
const parsedVal = parseFloat(value);
const baseFont = getBasePxFontSize(config);
if (isNaN(parsedVal)) throwSizeError(token.name, value, 'pt');
return (parsedVal * baseFont).toFixed(2) + 'f';
},
},
/**
* 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.0.sp"
* ```
*
* @memberof Transforms
*/
[transforms.sizeComposeRemToSp]: {
type: value,
filter: isFontSize,
transform: function (token, config, options) {
const value = options.usesDtcg ? token.$value : token.value;
const parsedVal = parseFloat(value);
const baseFont = getBasePxFontSize(config);
if (isNaN(parsedVal)) throwSizeError(token.name, 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.0.dp"
* ```
*
* @memberof Transforms
*/
[transforms.sizeComposeRemToDp]: {
type: value,
filter: isDimension,
transform: function (token, config, options) {
const value = options.usesDtcg ? token.$value : token.value;
const parsedVal = parseFloat(value);
const baseFont = getBasePxFontSize(config);
if (isNaN(parsedVal)) throwSizeError(token.name, 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.0em"
* ```
*
* @memberof Transforms
*/
[transforms.sizeComposeEm]: {
type: value,
filter: isFontSize,
transform: function (token, _, options) {
const value = options.usesDtcg ? token.$value : token.value;
const parsedVal = parseFloat(value);
if (isNaN(parsedVal)) throwSizeError(token.name, value, 'em');
return parsedVal + '.em';
},
},
/**
* 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 value = options.usesDtcg ? token.$value : token.value;
const parsedVal = parseFloat(value);
const baseFont = getBasePxFontSize(config);
if (isNaN(parsedVal)) throwSizeError(token.name, 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 value = options.usesDtcg ? token.$value : token.value;
const parsedVal = parseFloat(value);
const baseFont = getBasePxFontSize(config);
if (isNaN(parsedVal)) throwSizeError(token.name, 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"
* "1rem"
* ```
*
* @memberof Transforms
*/
[transforms.sizePxToRem]: {
type: value,
filter: (token, options) => isDimension(token, options) || isFontSize(token, options),
transform: (token, config, options) => {
const value = options.usesDtcg ? token.$value : token.value;
const parsedVal = parseFloat(value);
const baseFont = getBasePxFontSize(config);
if (isNaN(parsedVal)) {
throwSizeError(token.name, value, 'rem');
}
if (parsedVal === 0) {
return '0';
}
return `${parsedVal / baseFont}rem`;
},
},
/**
* 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} match
* @param {string} variable */
function (match, 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 { color, width } = val;
let { style } = val;
// 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
if (typeof style === 'object') {
style = 'dashed';
}
return `${width ? `${width} ` : ''}${style ? `${style}` : 'none'}${color ? ` ${color}` : ''}`;
},
},
/**
* 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 = options.usesDtcg ? token.$value : token.value;
if (typeof val !== 'object') {
// already transformed to string
return val;
}
let { fontFamily } = val;
const { fontWeight, fontVariant, fontWidth, fontSize, fontStyle, lineHeight } = val;
const CSSShorthandProps = [
'fontStyle',
'fontVariant',
'fontWeight',
'fontWidth',
'fontSize',
'lineHeight',
'fontFamily',
];
const unknownProps = Object.keys(val).filter((key) => !CSSShorthandProps.includes(key));
if (unknownProps.length > 0) {
GroupMessages.add(
UNKNOWN_CSS_FONT_PROPS_WARNINGS,
`${unknownProps.join(', ')} for token at ${token.path.join('.')}${
token.filePath ? ` in ${token.filePath}` : ''
}`,
);
}
fontFamily = fontFamily ?? 'sans-serif';
return `${fontStyle ? `${fontStyle} ` : ''}${fontVariant ? `${fontVariant} ` : ''}${
fontWeight ? `${fontWeight} ` : ''
}${fontWidth ? `${fontWidth} ` : ''}${
fontSize ? `${fontSize}` : `${getBasePxFontSize(platform)}px`
}${lineHeight ? `/${lineHeight} ` : ' '}${fontFamily}`;
},
},
/**
* Turns transition tokens object-value into stringified CSS shorthand
* https://design-tokens.github.io/community-group/format/#border
*
* ```js
* // Matches: token.type === 'transition'
* // Returns:
* "200ms linear 50ms"
* ```.
*
* @memberof Transforms
*/
[transforms.transitionCssShorthand]: {
type: value,
// transition properties can be references
transitive: true,
filter: (token, options) => (options.usesDtcg ? token.$type : token.type) === 'transition',
transform: (token, _, options) => {
const val = options.usesDtcg ? token.$value : token.value;
if (typeof val !== 'object') {
// already transformed to string
return val;
}
const { duration, delay, timingFunction } = val;
return `${duration} ${timingFunction} ${delay}`;
},
},
/**
* Turns shadow tokens object-value into stringified CSS shorthand
* https://design-tokens.github.io/community-group/format/#shadow
*
* ```js
* // Matches: token.type === 'shadow'
* // Returns:
* "inset 2px 4px 10px 5px #000000"
* ```.
*
* @memberof Transforms
*/
[transforms.shadowCssShorthand]: {
type: value,
// shadow properties can be references
transitive: true,
filter: (token, options) => (options.usesDtcg ? token.$type : token.type) === 'shadow',
transform: (token, _, options) => {
const val = options.usesDtcg ? token.$value : token.value;
if (typeof val !== 'object') {
// already transformed to string
return val;
}
/** @param {any} val */
const stringifyShadow = (val) => {
// check if the shadows are objects, they might already be transformed to strings if they were refs
if (typeof val !== 'object') {
return val;
}
const { type, color, offsetX, offsetY, blur, spread } = val;
return `${type ? `${type} ` : ''}${offsetX ?? 0} ${offsetY ?? 0} ${blur ?? 0} ${
spread ? `${spread} ` : ''
}${color ?? `#000000`}`;
};
if (Array.isArray(val)) {
return val.map(stringifyShadow).join(', ');
}
return stringifyShadow(val);
},
},
/**
* Wraps the value in a CSS url() function https://developer.mozilla.org/en-US/docs/Web/CSS/url
*
* ```js
* // Matches: token.type === 'asset'
* // Returns:
* url("https://www.example.com/style.css")
* ```
*
* @memberof Transforms
*/
[transforms.assetUrl]: {
type: value,
filter: isAsset,
transform: function (token, _, options) {
return `url("${(options.usesDtcg ? token.$value : token.value).replace(/"/g, `\\"`)}")`;
},
},
/**
* Wraps the value in a double-quoted string and prepends an '@' to make a string literal.
*
* ```js
* // Matches: token.type === 'asset'
* // Returns:
* 'IyBlZGl0b3Jjb25maWcub3JnCnJvb3QgPSB0cnVlCgpbKl0KaW5kZW50X3N0eWxlID0gc3BhY2UKaW5kZW50X3NpemUgPSAyCmVuZF9vZl9saW5lID0gbGYKY2hhcnNldCA9IHV0Zi04CnRyaW1fdHJhaWxpbmdfd2hpdGVzcGFjZSA9IHRydWUKaW5zZXJ0X2ZpbmFsX25ld2xpbmUgPSB0cnVlCgpbKi5tZF0KdHJpbV90cmFpbGluZ193aGl0ZXNwYWNlID0gZmFsc2U='
* ```
*
* @memberof Transforms
*/
[transforms.assetBase64]: {
type: value,
filter: isAsset,
transform: function (token, _, options, vol) {
return convertToBase64(options.usesDtcg ? token.$value : token.value, vol);
},
},
/**
* Prepends the local file path
*
* ```js
* // Matches: token.type === 'asset'
* // Returns:
* "path/to/file/asset.png"
* ```
*
* @memberof Transforms
*/
[transforms.assetPath]: {
type: value,
filter: isAsset,
transform: function (token, _, options) {
return join(process?.cwd() ?? '/', options.usesDtcg ? token.$value : token.value);
},
},
/**
* Wraps the value in a double-quoted string and prepends an '@' to make a string literal.
*
* ```objective-c
* // Matches: token.type === 'asset'
* // Returns: \@"string"
* ```
*
* @memberof Transforms
*/
[transforms.assetObjCLiteral]: {
type: value,
filter: isAsset,
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 === 'asset'
* // Returns: "string"
* ```
*
* @memberof Transforms
*/
[transforms.assetSwiftLiteral]: {
type: value,
filter: isAsset,
transform: (token, _, options) => wrapValueWithDoubleQuote(token, options),
},
/**
* Transforms the value into a Flutter Color object using 8-digit hex with the alpha chanel on start
* ```js
* // Matches: token.type === 'color'
* // Returns:
* Color(0xFF00FF5F)
* ```
* @memberof Transforms
*
*/
[transforms.colorHex8flutter]: {
type: value,
filter: isColor,
transform: function (token, _, options) {
const str = Color(options.usesDtcg ? token.$value : token.value)
.toHex8()
.toUpperCase();
return `Color(0x${str.slice(6)}${str.slice(0, 6)})`;
},
},
/**
* Wraps the value in a double-quoted string to make a string literal.
*
* ```dart
* // Matches: token.type === 'content'
* // Returns: "string"
* ```
*
* @memberof Transforms
*/
[transforms.contentFlutterLiteral]: {
type: value,
filter: isContent,
transform: (token, _, options) => wrapValueWithDoubleQuote(token, options),
},
/**
* Wraps the value in a double-quoted string to make a string literal.
*
* ```dart
* // Matches: token.type === 'asset'
* // Returns: "string"
* ```
*
* @memberof Transforms
*/
[transforms.assetFlutterLiteral]: {
type: value,
filter: isAsset,
transform: (token, _, options) => wrapValueWithDoubleQuote(token, options),
},
/**
* 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 baseFont = getBasePxFontSize(config);
return (parseFloat(options.usesDtcg ? token.$value : token.value) * baseFont).toFixed(2);
},
},
};