style-dictionary
Version:
Style once, use everywhere. A build system for creating cross-platform styles.
171 lines (155 loc) • 6.34 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 getName from './getName.js';
import usesReferences from './usesReferences.js';
import { regexCaptureGroups } from './createReferenceRegex.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').TransformedTokens} Tokens
* @typedef {import('../../../types/DesignToken.d.ts').TransformedToken} Token
*/
/**
* Public API wrapper around the functon below this one
* @param {string} value
* @param {Map<string, Token>} tokenMap
* @param {RefOpts} [opts]
* @returns {unknown}
*/
export function resolveReferences(value, tokenMap, 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, tokenMap, opts);
}
/**
* Utility to resolve references inside a string value
* @param {string} value
* @param {Map<string, Token>} tokenMap
* @param {RefOptsInternal} [opts]
* @returns {unknown}
*/
export function _resolveReferences(
value,
tokenMap,
{
usesDtcg = false,
warnImmediately = true,
// for internal usage
ignorePaths = new Set(),
current_context = '',
stack = [],
foundCirc = {},
firstIteration = true,
objectsOnly = false,
} = {},
) {
/** @type {unknown} */
let to_ret = value;
const valProp = usesDtcg ? '$value' : 'value';
const reg = regexCaptureGroups;
// 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) {
stack.push(getName([current_context]));
}
// this replace may be confusing, since we don't do anything with the return value
// we just use this as a regex match iterator, so read it like a "for each match"
value.replace(reg, (match) => {
// trim spaces between right after { or before } e.g. '{ foo.bar }' => '{foo.bar}'
let trimmedMatch = `{${match.slice(1, match.length - 1).trim()}}`;
if (ignorePaths.has(match)) {
return '';
}
stack.push(match);
const ref = tokenMap.get(trimmedMatch)?.[valProp];
/**
* @param {unknown} ref
*/
const replaceMatchWithRef = (ref) => {
if (objectsOnly && typeof ref !== 'object') {
return to_ret;
}
// If the token value is exclusively the ref
// we can put the ref in "as is", rather than stringifying.
// this way, if the reference is a primitive type like boolean, array, object, number,
// it wil remain that type.
if (match === to_ret) {
return ref;
}
// otherwise, stringify the ref (e.g. when it's a number ref inside a bigger string)
return `${to_ret}`.replace(match, `${ref}`);
};
if (typeof ref !== 'undefined') {
if (typeof ref === 'string' && usesReferences(ref)) {
// Recursive, therefore we can compute multi-layer variables like a = b, b = c, eventually a = c
// Compare to found circular references
if (Object.hasOwn(foundCirc, ref)) {
// If the current reference is a member of a circular reference, do nothing
} else if (stack.indexOf(ref) !== -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(ref);
// 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(ref);
// Add circ reference info to our list of warning messages
const warning = `Circular definition cycle for ${
current_context ?? ''
} => ${circStack.join(', ')}`;
if (warnImmediately) {
throw new Error(warning);
} else {
GroupMessages.add(PROPERTY_REFERENCE_WARNINGS, warning);
}
} else {
const nestedRef = _resolveReferences(ref, tokenMap, {
ignorePaths,
usesDtcg,
warnImmediately,
current_context,
stack,
foundCirc,
firstIteration: false,
});
to_ret = replaceMatchWithRef(nestedRef);
}
} else {
to_ret = replaceMatchWithRef(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 warning = `${
current_context ? `${current_context} ` : ''
}tries to reference ${value}, which is not defined.`;
if (warnImmediately) {
throw new Error(warning);
} else {
GroupMessages.add(PROPERTY_REFERENCE_WARNINGS, warning);
}
}
stack.pop();
return match;
});
return to_ret;
}