style-dictionary
Version:
Style once, use everywhere. A build system for creating cross-platform styles.
259 lines (239 loc) • 9.62 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 { resolveReferences } from './references/resolveReferences.js';
import usesReferences from './references/usesReferences.js';
import { deepmerge } from './deepmerge.js';
import isPlainObject from 'is-plain-obj';
/**
* @typedef {import('../../types/DesignToken.d.ts').DesignToken} DesignToken
* @typedef {import('../../types/DesignToken.d.ts').PreprocessedTokens} PreprocessedTokens
* @typedef {import('../../types/Config.d.ts').Expand} Expand
* @typedef {import('../../types/Config.d.ts').ExpandConfig} ExpandConfig
* @typedef {import('../../types/Config.d.ts').ExpandFilter} ExpandFilter
* @typedef {import('../../types/Config.d.ts').Config} Config
* @typedef {import('../../types/Config.d.ts').PlatformConfig} PlatformConfig
*/
export const DTCGTypesMap = {
// https://design-tokens.github.io/community-group/format/#border
border: {
style: 'strokeStyle',
width: 'dimension',
},
// https://design-tokens.github.io/community-group/format/#transition
transition: {
delay: 'duration',
// needs more discussion https://github.com/design-tokens/community-group/issues/103
timingFunction: 'cubicBezier',
},
// https://design-tokens.github.io/community-group/format/#shadow
shadow: {
offsetX: 'dimension',
offsetY: 'dimension',
blur: 'dimension',
spread: 'dimension',
},
// https://design-tokens.github.io/community-group/format/#gradient
gradient: {
position: 'number',
},
// https://design-tokens.github.io/community-group/format/#typography
typography: {
fontSize: 'dimension',
letterSpacing: 'dimension',
lineHeight: 'number',
},
// https://design-tokens.github.io/community-group/format/#object-value
strokeStyle: {
dashArray: 'dimension',
},
};
/**
* expandTypesMap and this function may be slightly confusing,
* refer to the unit tests for a better explanation
* @param {string} subtype
* @param {string} compositionType
* @param {Expand['typesMap']} expandTypesMap
* @returns {string}
*/
export function getTypeFromMap(subtype, compositionType, expandTypesMap = {}) {
const typeMap = deepmerge(DTCGTypesMap, expandTypesMap);
// the map might exist within the compositionType
const mapObjForComp = typeMap[compositionType];
// or instead, it may be on the top-level, independent of the compositionType
const mappedSubType = typeMap[subtype];
if (typeof mapObjForComp === 'object' && mapObjForComp[subtype]) {
return mapObjForComp[subtype];
// the type mapping might be on the top-level, independent of the compositionType
} else if (typeof mappedSubType === 'string') {
return mappedSubType;
}
return subtype;
}
/**
* @param {DesignToken} token
* @param {Config} opts
* @param {PlatformConfig} [platform]
*/
function shouldExpand(token, opts, platform) {
const expand = platform?.expand ?? opts.expand ?? false;
/** @type {ExpandFilter | boolean} */
let condition = false;
let reverse = false;
if (typeof expand === 'function' || typeof expand === 'boolean') {
condition = expand;
} else {
const type = /** @type {string} */ (opts.usesDtcg ? token.$type : token.type);
if (expand.include === undefined && expand.exclude === undefined) {
condition = true;
}
if (expand.include) {
condition =
typeof expand.include === 'function' ? expand.include : expand.include.includes(type);
}
if (/** @type {Expand} */ (expand).exclude) {
if (expand.include) {
throw Error(
'expand.include should not be combined with expand.exclude, use one or the other.',
);
}
condition =
typeof expand.exclude === 'function' ? expand.exclude : expand.exclude.includes(type);
reverse = true;
}
}
let result = condition;
if (typeof condition === 'function') {
result = condition(token, opts, platform);
}
return reverse ? !result : result;
}
/**
* @param {DesignToken} token
* @param {Config} opts
* @param {PlatformConfig} [platform]
*/
export function expandToken(token, opts, platform) {
const uses$ = opts.usesDtcg;
// create a copy of the token without the value/type, so that we have all the meta props
// which have to be inherited in the expanded tokens.
/** @type {Record<string, unknown>} */
const copyMeta = {};
Object.keys(token)
// either filter $value & $type, or value and type depending on whether $ is used
.filter(
(key) =>
!['$value', 'value', '$type', 'type']
.filter((key) => (uses$ ? key.startsWith('$') : !key.startsWith('$')))
.includes(key),
)
.forEach((key) => {
copyMeta[key] = token[key];
});
const value = uses$ ? token.$value : token.value;
// the $type and type may both be missing if the $type is coming from an ancestor token group,
// however, prior to expand and preprocessors, we run a step so missing $type is added from the closest ancestor
const compositionType = /** @type {string} */ (token.$type ?? token.type);
/** @type {PreprocessedTokens} */
const expandedTokenObj = {};
/** @type {Expand['typesMap']} */
let typesMap = {};
const expand = platform?.expand ?? opts.expand;
if (typeof expand === 'object') {
typesMap = expand.typesMap ?? {};
}
// array of objects is also valid e.g. multi-shadow values
// https://github.com/design-tokens/community-group/issues/100 there seems to be a consensus for this
// so this code adds forward-compatibility with that
const _value = Array.isArray(value) ? value : [value];
_value.forEach((objectVal, index, arr) => {
let expandedTokenObjRef = expandedTokenObj;
// more than 1 means multi-value, meaning we should add nested token group
// with index to the expanded result
if (arr.length > 1) {
expandedTokenObj[index + 1] = {};
expandedTokenObjRef = expandedTokenObjRef[index + 1];
}
Object.entries(objectVal).forEach(([key, value]) => {
expandedTokenObjRef[key] = {
...copyMeta,
[`${uses$ ? '$' : ''}value`]: value,
[`${uses$ ? '$' : ''}type`]: getTypeFromMap(key, compositionType, typesMap),
};
if (Array.isArray(expandedTokenObjRef[key].path)) {
// if we're expanding on platform level, we already have a path property
// we will need to adjust this by adding the new keys of th expanded tokens to the path array
expandedTokenObjRef[key].path = [...expandedTokenObjRef[key].path, key];
}
});
});
return expandedTokenObj;
}
/**
* @param {PreprocessedTokens | DesignToken} slice
* @param {PreprocessedTokens} original
* @param {Config} opts
* @param {PlatformConfig} [platform]
*/
function expandTokensRecurse(slice, original, opts, platform) {
for (const key in slice) {
const token = slice[key];
if (!isPlainObject(token) || token === null) {
continue;
}
const uses$ = opts.usesDtcg;
let value = uses$ ? token.$value : token.value;
if (value) {
// if our token is a ref, we have to resolve it first in order to expand its value
if (typeof value === 'string' && usesReferences(value)) {
try {
value = resolveReferences(value, original, { usesDtcg: uses$ });
} catch {
// do nothing, references may be broken but now is not the time to
// complain about it, as we're just doing this here so we can expand
// tokens that reference object-value tokens that need to be expanded
}
}
if (
isPlainObject(value) ||
// support multi-value arrays where each item is an object, e.g. shadow tokens
(Array.isArray(value) && value.every((sub) => isPlainObject(sub)))
) {
// if the resolved value is an object, then we must assume it could get expanded and
// we must set the value to the resolved value, since the reference might be broken after expansion
slice[key][uses$ ? '$value' : 'value'] = value;
if (shouldExpand(token, opts, platform)) {
slice[key] = expandToken(token, opts, platform);
}
}
}
// We might expect an else statement here on the line above, but we also want
// to recurse if a value is present so that we support expanding nested object values,
// e.g. a border can have a style prop (strokeStyle) which itself
// can also be an object value with dashArray and lineCap props.
// More info: https://design-tokens.github.io/community-group/format/#example-border-composite-token-examples
expandTokensRecurse(slice[key], original, opts, platform);
}
}
/**
* @param {PreprocessedTokens} dictionary
* @param {Config} opts
* @param {PlatformConfig} [platform]
*/
export function expandTokens(dictionary, opts, platform) {
// create a copy in which we will do mutations
const copy = structuredClone(dictionary);
// create a separate copy to check as the original object
const original = structuredClone(dictionary);
expandTokensRecurse(copy, original, opts, platform);
return copy;
}