style-dictionary
Version:
Style once, use everywhere. A build system for creating cross-platform styles.
196 lines (174 loc) • 7.36 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 GroupMessages from '../groupMessages.js';
import getPathFromName from './getPathFromName.js';
import getName from './getName.js';
import getValueByPath from './getValueByPath.js';
import usesReferences from './usesReferences.js';
import createReferenceRegex from './createReferenceRegex.js';
import defaults from './defaults.js';
const PROPERTY_REFERENCE_WARNINGS = GroupMessages.GROUP.PropertyReferenceWarnings;
/**
* @typedef {import('../../../types/Config.d.ts').ResolveReferencesOptions} RefOpts
* @typedef {import('../../../types/Config.d.ts').ResolveReferencesOptionsInternal} RefOptsInternal
* @typedef {import('../../../types/DesignToken.d.ts').PreprocessedTokens} Tokens
* @typedef {import('../../../types/DesignToken.d.ts').DesignToken} Token
*/
/**
* Public API wrapper around the functon below this one
* @param {string} value
* @param {Tokens} tokens
* @param {RefOpts} [opts]
* @returns {string|number|undefined}
*/
export function resolveReferences(value, tokens, opts) {
// when using this public API / util, we always throw warnings immediately rather than
// putting them in the GroupMessages PROPERTY_REFERENCE_WARNINGS to collect and throw later on.
return _resolveReferences(value, tokens, opts);
}
/**
* Utility to resolve references inside a string value
* @param {string} value
* @param {Tokens} tokens
* @param {RefOptsInternal} [opts]
* @returns {string|number|undefined}
*/
export function _resolveReferences(
value,
tokens,
{
regex,
separator = defaults.separator,
opening_character = defaults.opening_character,
closing_character = defaults.closing_character,
usesDtcg = false,
warnImmediately = true,
// for internal usage
ignorePaths = [],
current_context = [],
stack = [],
foundCirc = {},
firstIteration = true,
} = {},
) {
const reg = regex ?? createReferenceRegex({ opening_character, closing_character, separator });
/** @type {Token|string|number|undefined} */
let to_ret = value;
/** @type {Token|string|number|undefined} */
let ref;
const valProp = usesDtcg ? '$value' : 'value';
// When we know the current context:
// the key associated with the value that we are resolving the reference for
// Then we can push this to the stack to improve our circular reference warnings
// by starting them with the key
if (firstIteration && current_context.length > 0) {
stack.push(getName(current_context));
}
/**
* Replace the reference inline, but don't replace the whole string because
* references can be part of the value such as "1px solid {color.border.light}"
*/
value.replace(reg, (match, /** @type {string} */ variable) => {
variable = variable.trim();
// Find what the value is referencing
const pathName = getPathFromName(variable, separator);
const refHasValue = valProp === pathName[pathName.length - 1];
if (refHasValue && ignorePaths.indexOf(variable) !== -1) {
return '';
} else if (!refHasValue && ignorePaths.indexOf(`${variable}.${valProp}`) !== -1) {
return '';
}
stack.push(variable);
ref = getValueByPath(pathName, tokens);
// If the reference doesn't end in 'value'
// and
// the reference points to someplace that has a `value` attribute
// we should take the '.value' of the reference
// per the DTCG draft spec where references do not have .value
// https://design-tokens.github.io/community-group/format/#aliases-references
if (!refHasValue && ref && Object.hasOwn(ref, valProp)) {
ref = ref[valProp];
}
if (typeof ref !== 'undefined') {
if (typeof ref === 'string' || typeof ref === 'number') {
to_ret = value.replace(match, `${ref}`);
// Recursive, therefore we can compute multi-layer variables like a = b, b = c, eventually a = c
if (usesReferences(to_ret, reg)) {
const reference = to_ret.slice(1, -1);
// Compare to found circular references
if (Object.hasOwn(foundCirc, reference)) {
// If the current reference is a member of a circular reference, do nothing
} else if (stack.indexOf(reference) !== -1) {
// If the current stack already contains the current reference, we found a new circular reference
// chop down only the circular part, save it to our circular reference info, and spit out an error
// Get the position of the existing reference in the stack
const stackIndexReference = stack.indexOf(reference);
// Get the portion of the stack that starts at the circular reference and brings you through until the end
const circStack = stack.slice(stackIndexReference);
// For all the references in this list, add them to the list of references that end up in a circular reference
circStack.forEach(function (key) {
foundCirc[key] = true;
});
// Add our found circular reference to the end of the cycle
circStack.push(reference);
// Add circ reference info to our list of warning messages
const warning = `Circular definition cycle: ${circStack.join(', ')}`;
if (warnImmediately) {
throw new Error(warning);
} else {
GroupMessages.add(
PROPERTY_REFERENCE_WARNINGS,
'Circular definition cycle: ' + circStack.join(', '),
);
}
} else {
to_ret = _resolveReferences(to_ret, tokens, {
regex: reg,
ignorePaths,
usesDtcg,
warnImmediately,
current_context,
separator,
stack,
foundCirc,
firstIteration: false,
});
}
}
// if evaluated value is a number and equal to the reference, we want to keep the type
if (typeof ref === 'number' && ref.toString() === to_ret) {
to_ret = ref;
}
} else {
// if evaluated value is not a string or number, we want to keep the type
to_ret = ref;
}
} else {
// User might have passed current_context option which is path (arr) pointing to key
// that this value is associated with, helpful for debugging
const context = getName(current_context, { separator });
const warning = `${
context ? `${context} ` : ''
}tries to reference ${variable}, which is not defined.`;
if (warnImmediately) {
throw new Error(warning);
} else {
GroupMessages.add(PROPERTY_REFERENCE_WARNINGS, warning);
}
to_ret = ref;
}
stack.pop();
return '';
});
return to_ret;
}