779 lines
23 KiB
JavaScript
779 lines
23 KiB
JavaScript
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
/* eslint-env node */
|
|
|
|
const StyleDictionary = require("style-dictionary");
|
|
|
|
const COLLECTIONS = {
|
|
colors: "Colors",
|
|
primitives: "Primitives",
|
|
theme: "Theme",
|
|
hcmTheme: "HCM Theme",
|
|
};
|
|
|
|
const HCM_VALUES = [
|
|
"ActiveText",
|
|
"ButtonBorder",
|
|
"ButtonFace",
|
|
"ButtonText",
|
|
"Canvas",
|
|
"CanvasText",
|
|
"Field",
|
|
"FieldText",
|
|
"GrayText",
|
|
"Highlight",
|
|
"HighlightText",
|
|
"LinkText",
|
|
"Mark",
|
|
"MarkText",
|
|
"SelectedItem",
|
|
"SelectedItemText",
|
|
"AccentColor",
|
|
"AccentColorText",
|
|
"VisitedText",
|
|
];
|
|
|
|
StyleDictionary.registerTransform({
|
|
type: "name",
|
|
name: "name/figma",
|
|
transformer: token => token.path.join("/").replace("/@base", ""),
|
|
});
|
|
|
|
/**
|
|
* Determines if a given value is an arbitrarily deeply nested object
|
|
* that satisfies the following conditions:
|
|
* - Each object in the nesting hierarchy has exactly one key-value pair.
|
|
* - The last key in the nesting hierarchy is "default".
|
|
* - The value associated with the "default" key is a primitive (not an object).
|
|
* - The value associated with the "default" key is not "currentColor".
|
|
*
|
|
* @param {object} value - The value to check, expected to be an object.
|
|
* @returns {boolean} Returns `true` if the object matches the criteria,
|
|
*/
|
|
function isNestedDefaultObject(value) {
|
|
let current = value;
|
|
while (typeof current === "object") {
|
|
const keys = Object.keys(current);
|
|
if (keys.length !== 1) {
|
|
return false;
|
|
}
|
|
const key = keys[0];
|
|
if (key === "default") {
|
|
if (typeof current[key] === "object") {
|
|
return false;
|
|
}
|
|
if (current[key] === "currentColor") {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
current = current[key];
|
|
}
|
|
return false;
|
|
}
|
|
|
|
StyleDictionary.registerTransform({
|
|
type: "attribute",
|
|
name: "attribute/figma",
|
|
transformer: token => {
|
|
// Collection attribute
|
|
let collection = COLLECTIONS.theme;
|
|
|
|
if (
|
|
(typeof token.value !== "object" || isNestedDefaultObject(token.value)) &&
|
|
token.value !== "currentColor"
|
|
) {
|
|
if (token.path[0] === "color") {
|
|
collection = COLLECTIONS.colors;
|
|
} else {
|
|
collection = COLLECTIONS.primitives;
|
|
}
|
|
}
|
|
|
|
// willBeDestructured attribute
|
|
let willBeDestructured = false;
|
|
const originalVal = token.original.value;
|
|
if (
|
|
attemptShadowDestructuring(token, originalVal) ||
|
|
attemptPaddingMarginDestructuring(token, originalVal)
|
|
) {
|
|
willBeDestructured = true;
|
|
}
|
|
|
|
return { collection, willBeDestructured };
|
|
},
|
|
});
|
|
|
|
// ---------
|
|
// Style Dictionary Custom Format
|
|
// ---------
|
|
|
|
/**
|
|
* Formats design tokens based on the provided arguments and options.
|
|
*
|
|
* @param {string} collection - The name of the token collection to filter by.
|
|
* @returns {Function} A function that takes an object with `dictionary` and `options` properties
|
|
* and returns a formatted JSON string of the tokens.
|
|
*/
|
|
const formatTokens = collection => args => {
|
|
let dictionary = Object.assign({}, args.dictionary);
|
|
let tokens = [];
|
|
|
|
const filter = mergeFilters(
|
|
defaultFilter,
|
|
token => token.attributes?.collection === collection
|
|
);
|
|
|
|
dictionary.allTokens.forEach(token => {
|
|
let originalVal = token.original.value;
|
|
|
|
if (originalVal === undefined) {
|
|
throw new Error(
|
|
`[formatTokens] Token ${token.name} has an undefined original value. Please check your tokens.`
|
|
);
|
|
}
|
|
|
|
// If the current token references another token that will be destructured,
|
|
// we skip it altogether
|
|
if (dictionary.usesReference(originalVal)) {
|
|
const references = dictionary.getReferences(originalVal);
|
|
if (references.some(ref => ref.attributes.willBeDestructured)) {
|
|
console.warn(
|
|
`[formatTokens] Skipping token ${token.name} because it references a token that will be destructured`
|
|
);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// If the token is a CSS box-shadow shorthand, attempt to destructure it
|
|
// into its subtokens and process each subtoken
|
|
const potentialShadowTokens = attemptShadowDestructuring(
|
|
token,
|
|
originalVal
|
|
);
|
|
if (potentialShadowTokens) {
|
|
potentialShadowTokens.forEach(
|
|
({ token: sToken, originalVal: sOriginalVal }) => {
|
|
// Check if the subtoken should be filtered out
|
|
if (!filter(sToken)) {
|
|
return;
|
|
}
|
|
// Transform the subtoken value and add it to the tokens array
|
|
let formattedToken = transformTokenValue(
|
|
sToken,
|
|
sOriginalVal,
|
|
dictionary
|
|
);
|
|
tokens.push(formattedToken);
|
|
}
|
|
);
|
|
return;
|
|
}
|
|
|
|
// If the token is a padding/margin shorthand, attempt to destructure it
|
|
const potentialPaddingMarginTokens = attemptPaddingMarginDestructuring(
|
|
token,
|
|
originalVal
|
|
);
|
|
if (potentialPaddingMarginTokens) {
|
|
potentialPaddingMarginTokens.forEach(
|
|
({ token: sToken, originalVal: sOriginalVal }) => {
|
|
// Check if the subtoken should be filtered out
|
|
if (!filter(sToken)) {
|
|
return;
|
|
}
|
|
// Transform the subtoken value and add it to the tokens array
|
|
let formattedToken = transformTokenValue(
|
|
sToken,
|
|
sOriginalVal,
|
|
dictionary
|
|
);
|
|
tokens.push(formattedToken);
|
|
}
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Check if the token should be filtered out
|
|
if (!filter(token)) {
|
|
return;
|
|
}
|
|
// Otherwise transform the original token value and add it to the tokens array
|
|
let formattedToken = transformTokenValue(token, originalVal, dictionary);
|
|
tokens.push(formattedToken);
|
|
});
|
|
|
|
if (!tokens.length) {
|
|
return "";
|
|
}
|
|
|
|
dictionary.allTokens = dictionary.allProperties = tokens;
|
|
return (
|
|
"{\n" +
|
|
dictionary.allTokens
|
|
.map(function (token) {
|
|
return ` "${token.name}": ${JSON.stringify(
|
|
args.options.usesDtcg ? token.$value : token.value,
|
|
null,
|
|
2
|
|
).replace(/\n/g, "\n ")}`;
|
|
})
|
|
.join(",\n") +
|
|
"\n}" +
|
|
"\n"
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Transforms the value of a design token by resolving references, handling `calc()` expressions,
|
|
* and applying specific transformations for light, dark, and forced color modes.
|
|
*
|
|
* @param {object} token - The token object containing metadata and the value to transform.
|
|
* @param {string|object} originalVal - The original value of the token, which can be a string or an object.
|
|
* @param {object} dictionary - Dictionary object from Style Dictionary
|
|
* @returns {object} - A new token object with the transformed value.
|
|
*/
|
|
function transformTokenValue(token, originalVal, dictionary) {
|
|
let newValue = originalVal;
|
|
|
|
if (typeof token.value === "object") {
|
|
const brandValue = getNestedBrandColor(originalVal);
|
|
const forcedColorsValue = getNestedSystemColor(originalVal);
|
|
|
|
// If this token got assigned to the primitive collection, we know it
|
|
// only contains a single value, so we can just the light value
|
|
if (token.attributes?.collection === COLLECTIONS.primitives) {
|
|
newValue = brandValue?.light;
|
|
} else {
|
|
newValue = {
|
|
light: brandValue?.light
|
|
? replaceReferences(dictionary, brandValue?.light)
|
|
: "transparent",
|
|
dark: brandValue?.dark
|
|
? replaceReferences(dictionary, brandValue?.dark)
|
|
: "transparent",
|
|
forcedColors: replaceReferences(dictionary, forcedColorsValue),
|
|
};
|
|
|
|
if (
|
|
newValue.forcedColors === undefined &&
|
|
newValue.light === newValue.dark
|
|
) {
|
|
newValue.forcedColors = newValue.light;
|
|
} else if (newValue.forcedColors === undefined) {
|
|
newValue.forcedColors = "transparent";
|
|
}
|
|
}
|
|
} else if (dictionary.usesReference(newValue)) {
|
|
newValue = replaceReferences(dictionary, newValue);
|
|
}
|
|
|
|
if (typeof newValue === "object") {
|
|
Object.keys(newValue).forEach(key => {
|
|
newValue[key] = potentiallyTransformValue(token, newValue[key]);
|
|
});
|
|
} else {
|
|
newValue = potentiallyTransformValue(token, newValue);
|
|
}
|
|
|
|
return { ...token, value: newValue };
|
|
}
|
|
|
|
function potentiallyTransformValue(token, value) {
|
|
// We convert number strings without units to numbers and since Figma's
|
|
// spaces everything in pixels, we convert pixel values to numbers too
|
|
const numberOrPxRegex = /^-?\d*\.?\d+(px)?$/;
|
|
if (typeof value === "string" && numberOrPxRegex.test(value)) {
|
|
const numberValue = parseFloat(value);
|
|
if (isNaN(numberValue)) {
|
|
throw new Error(`Failed to parse value: ${value}`);
|
|
}
|
|
return numberValue;
|
|
}
|
|
// Figma expects opacity values to be in the range of 0-100
|
|
if (
|
|
typeof value === "number" &&
|
|
value >= 0 &&
|
|
value <= 1 &&
|
|
token.path.includes("opacity")
|
|
) {
|
|
return Math.round(value * 100);
|
|
}
|
|
return value;
|
|
}
|
|
|
|
/**
|
|
* Replaces references and resolves `calc()` expressions in a given value.
|
|
*
|
|
* This function processes a value to replace references with their actual values
|
|
* and resolves any `calc()` expressions by evaluating them. It also handles
|
|
* specific cases for high contrast mode (HCM) values.
|
|
*
|
|
* @param {object} dictionary - The Style Dictionary instance containing the token references.
|
|
* @param {string | undefined} value - The value to process.
|
|
*/
|
|
function replaceReferences(dictionary, value) {
|
|
if (value === undefined) {
|
|
return value;
|
|
}
|
|
|
|
// Replace calc() expressions with their computed values
|
|
if (typeof value === "string") {
|
|
value = value.replace(/calc\(([^()]+)\)/g, (_, calcContent) => {
|
|
// Replace references inside the calc() content with their actual values
|
|
dictionary.getReferences(calcContent).forEach(ref => {
|
|
calcContent = calcContent.replace(
|
|
`{${ref.path.join(".")}}`, // Reference path in the format {path.to.reference}
|
|
ref.value // Replace with the actual value of the reference
|
|
);
|
|
});
|
|
// Resolve the calc() expression to its computed value
|
|
const calcValue = resolveCssCalc(calcContent);
|
|
return calcValue;
|
|
});
|
|
}
|
|
|
|
// Check if the value contains any references
|
|
if (dictionary.usesReference(value)) {
|
|
// Replace the style dictionary references with
|
|
// the format expected by the figma import script
|
|
dictionary.getReferences(value).forEach(ref => {
|
|
value = value.replace(
|
|
`{${ref.path.join(".")}}`,
|
|
`{${ref.attributes.collection}$${ref.name}}`
|
|
);
|
|
});
|
|
}
|
|
|
|
// If the value matches any of the predefined HCM values, replace whith
|
|
// reference expected by the figma import script
|
|
if (HCM_VALUES.includes(value)) {
|
|
value = `{${COLLECTIONS.hcmTheme}$${value}}`;
|
|
}
|
|
|
|
return value;
|
|
}
|
|
|
|
/**
|
|
* Retrieves the nested system color value from a token object.
|
|
* The function traverses through the token's properties (`forcedColors`, `prefersContrast`, `default`)
|
|
* until it resolves to a non-object value.
|
|
*
|
|
* @param {object} token - The token object containing nested system color definitions.
|
|
* @returns {string|number|boolean|null|undefined} - The resolved system color value.
|
|
*/
|
|
function getNestedSystemColor(token) {
|
|
let current = token;
|
|
while (typeof current === "object") {
|
|
current =
|
|
current.forcedColors ?? current.prefersContrast ?? current.default;
|
|
}
|
|
return current;
|
|
}
|
|
|
|
/**
|
|
* Retrieves the nested brand color from a token object. The function traverses
|
|
* the token structure to find and return an object containing `light` and `dark`
|
|
* color values. If the token is a primitive value, it returns the same value
|
|
* for both `light` and `dark`. If no valid color object is found, it returns `undefined`.
|
|
*
|
|
* @param {object|string|number} token - The token object or value to extract the brand color from.
|
|
* @returns {{light: string|number, dark: string|number}|undefined} An object containing `light` and `dark`
|
|
* color values, or `undefined` if no valid color is found.
|
|
*/
|
|
function getNestedBrandColor(token) {
|
|
const stack = [token];
|
|
while (stack.length) {
|
|
const node = stack.pop();
|
|
if (typeof node !== "object") {
|
|
return { light: node, dark: node };
|
|
}
|
|
if (typeof node === "object") {
|
|
if (node.light && node.dark) {
|
|
return node;
|
|
}
|
|
if (node.brand) {
|
|
stack.push(node.brand);
|
|
}
|
|
if (node.default) {
|
|
stack.push(node.default);
|
|
}
|
|
}
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
/**
|
|
* Attempts to destructure a shadow token into its individual components.
|
|
* This function processes a token with a potential shadow value,
|
|
* and attempts to parse it into subtokens with destructured shadow properties.
|
|
*
|
|
* @param {object} token - The token object to process.
|
|
* @param {string} originalVal - The original value of the token, expected to be a string.
|
|
* @returns {Array<object>|undefined} An array of new with destructured shadow properties,
|
|
* or `undefined` if the token is not a shadow or cannot be parsed.
|
|
*/
|
|
function attemptShadowDestructuring(token, originalVal) {
|
|
if (!token.path.includes("shadow") || typeof originalVal !== "string") {
|
|
return undefined;
|
|
}
|
|
|
|
// check if originalVal contains at least a space
|
|
if (!originalVal.includes(" ")) {
|
|
return undefined;
|
|
}
|
|
|
|
let shadows;
|
|
|
|
try {
|
|
shadows = parseBoxShadow(originalVal);
|
|
} catch (e) {
|
|
console.warn("[attemptShadowDestructuring] Error parsing shadow:", e);
|
|
return undefined;
|
|
}
|
|
|
|
if (shadows.length === 0) {
|
|
return undefined;
|
|
}
|
|
|
|
const subtokens = shadows.flatMap((shadow, index) => {
|
|
const path =
|
|
shadows.length > 1 ? [...token.path, `shadow-${index + 1}`] : token.path;
|
|
return Object.entries(shadow).map(([key, value]) => {
|
|
// Every part of the shadow but the color can be put
|
|
// in the primitive collection
|
|
let collection = token.attributes?.collection;
|
|
if (key !== "color") {
|
|
collection = COLLECTIONS.primitives;
|
|
}
|
|
|
|
const copy = {
|
|
...token,
|
|
path: [...path, key].filter(filterBase),
|
|
name: [...path, key].filter(filterBase).join("/"),
|
|
value,
|
|
original: { ...token.original, value },
|
|
attributes: {
|
|
...token.attributes,
|
|
collection,
|
|
},
|
|
};
|
|
return { token: copy, originalVal: value };
|
|
});
|
|
});
|
|
|
|
return subtokens;
|
|
}
|
|
|
|
function attemptPaddingMarginDestructuring(token, originalVal) {
|
|
if (
|
|
typeof originalVal !== "string" ||
|
|
(!token.path.includes("padding") && !token.path.includes("margin"))
|
|
) {
|
|
return undefined;
|
|
}
|
|
const parts = originalVal.split(/(?<!\([^\s]*)\s+/);
|
|
|
|
// return undefined if only 1 part
|
|
if (parts.length === 1) {
|
|
return undefined;
|
|
}
|
|
// throw if more than 4
|
|
if (parts.length > 4) {
|
|
throw new Error(
|
|
`[attemptPaddingMarginDestructuring] Too many parts in ${originalVal}`
|
|
);
|
|
}
|
|
|
|
// if 2 parts make object with block and inline keys
|
|
// if 3 parts with block-start, inline and block-end keys
|
|
// if 4 parts with block-start, inline-start, block-end and inline-end keys
|
|
const result = {};
|
|
if (parts.length === 2) {
|
|
result.block = parts[0];
|
|
result.inline = parts[1];
|
|
} else if (parts.length === 3) {
|
|
result.blockStart = parts[0];
|
|
result.inline = parts[1];
|
|
result.blockEnd = parts[2];
|
|
} else if (parts.length === 4) {
|
|
result.blockStart = parts[0];
|
|
result.inlineStart = parts[1];
|
|
result.blockEnd = parts[2];
|
|
result.inlineEnd = parts[3];
|
|
}
|
|
|
|
return Object.entries(result).map(([key, value]) => {
|
|
const copy = {
|
|
...token,
|
|
path: [...token.path, key].filter(filterBase),
|
|
name: [...token.path, key].filter(filterBase).join("/"),
|
|
value,
|
|
original: { ...token.original, value },
|
|
};
|
|
return { token: copy, originalVal: value };
|
|
});
|
|
}
|
|
|
|
// ---------
|
|
// CSS Parsing Functions
|
|
// ---------
|
|
|
|
/**
|
|
* Attempts to evaluate a CSS calc expression
|
|
*
|
|
* @param {string} expression - The CSS `calc()` expression to evaluate.
|
|
* It can include numbers, units (e.g., px, %, rem), and operators (+, -, *, /).
|
|
*
|
|
* @returns {string} - The evaluated result of the expression as a string, including the unit (if any).
|
|
*
|
|
* @throws {Error} - Throws an error if the expression is invalid or if multiple units are mixed.
|
|
*
|
|
* @example
|
|
* resolveCssCalc("2 * 5rem"); // returns "10rem"
|
|
* resolveCssCalc("8 / 2"); // returns "4"
|
|
* resolveCssCalc("2px + 5px"); // returns "7px"
|
|
* resolveCssCalc("1% + 5px"); // throws Error: Mixing units is not allowed
|
|
*/
|
|
function resolveCssCalc(expression) {
|
|
const unitRegex = /[a-zA-Z%]+/g;
|
|
const precedence = { "+": 1, "-": 1, "*": 2, "/": 2 };
|
|
|
|
// Tokenize the expression into numbers, units, and operators
|
|
const tokens = expression.match(/\d*\.?\d+[a-zA-Z%]*|[-+*/()]/g);
|
|
if (!tokens) {
|
|
throw new Error("[resolveCssCalc] Invalid expression");
|
|
}
|
|
|
|
// Collect all unique units found in the expression
|
|
const units = new Set();
|
|
const parsedTokens = tokens.map(token => {
|
|
const unit = token.match(unitRegex)?.[0] || ""; // Extract the unit from the token
|
|
if (unit) {
|
|
units.add(unit); // Add the unit to the set
|
|
}
|
|
return parseFloat(token) || token; // Parse the numeric value or keep the operator
|
|
});
|
|
|
|
// Ensure that only one type of unit is used in the expression
|
|
// If two units eliminate each other (e.g. 5px - 5px + 1rem) then that also fails, but why should we make such calculations.
|
|
if (units.size > 1) {
|
|
throw new Error("[resolveCssCalc] Mixing units is not allowed");
|
|
}
|
|
const resultUnit = units.size ? [...units][0] : "";
|
|
|
|
const output = [];
|
|
const operators = [];
|
|
|
|
// Function to compute the result of the top two numbers in the output stack
|
|
const compute = () => {
|
|
const b = output.pop();
|
|
const a = output.pop();
|
|
switch (operators.pop()) {
|
|
case "+":
|
|
output.push(a + b);
|
|
break;
|
|
case "-":
|
|
output.push(a - b);
|
|
break;
|
|
case "*":
|
|
output.push(a * b);
|
|
break;
|
|
case "/":
|
|
output.push(a / b);
|
|
break;
|
|
}
|
|
};
|
|
|
|
// Process each token in the expression
|
|
for (let i = 0; i < parsedTokens.length; i++) {
|
|
let token = parsedTokens[i];
|
|
// Push numbers to the output stack
|
|
if (typeof token === "number") {
|
|
output.push(token);
|
|
}
|
|
// Push opening parenthesis to the operators stack
|
|
else if (token === "(") {
|
|
operators.push(token);
|
|
}
|
|
// Compute closing parenthesis, until the matching opening parenthesis is found
|
|
else if (token === ")") {
|
|
while (operators.at(-1) !== "(") {
|
|
compute();
|
|
}
|
|
// Remove the opening parenthesis from the stack
|
|
operators.pop();
|
|
}
|
|
// If the token is an operator (+, -, *, /)
|
|
else {
|
|
// Handle signed numbers (e.g., "-5" or "+3")
|
|
if (
|
|
(token === "-" || token === "+") &&
|
|
(i === 0 || parsedTokens[i - 1] === "(")
|
|
) {
|
|
output.push(0); // Treat it as "0 - 5" or "0 + 3"
|
|
}
|
|
// While the precedence of the current operator is less than or equal to the operator on top of the stack,
|
|
// compute the result of the top two numbers in the output stack
|
|
while (
|
|
operators.length &&
|
|
precedence[token] <= precedence[operators.at(-1)]
|
|
) {
|
|
compute();
|
|
}
|
|
// Push the current operator to the operators stack
|
|
operators.push(token);
|
|
}
|
|
}
|
|
// Compute any remaining operations in the stacks
|
|
while (operators.length) {
|
|
compute();
|
|
}
|
|
|
|
if (isNaN(output[0])) {
|
|
throw new Error(
|
|
"[resolveCssCalc] Resolving math expression resulted in NaN"
|
|
);
|
|
}
|
|
return output[0] + resultUnit;
|
|
}
|
|
|
|
const DEFAULT_SHADOW = {
|
|
x: "0",
|
|
y: "0",
|
|
blur: "0",
|
|
spread: "0",
|
|
color: "transparent",
|
|
};
|
|
|
|
/**
|
|
* Parses a CSS box-shadow string and returns an array of shadow objects.
|
|
*
|
|
* @param {string} input - The box-shadow string to parse. Must be a valid CSS box-shadow value.
|
|
* @returns {Array<object>} An array of objects representing the parsed box-shadow values.
|
|
* @throws {Error} Throws an error if the input is not a string or if the box-shadow syntax is invalid.
|
|
*/
|
|
function parseBoxShadow(input) {
|
|
if (typeof input !== "string") {
|
|
throw new Error("Input must be a string");
|
|
}
|
|
// Regex to split multiple box-shadow definitions, ignoring commas inside parentheses
|
|
const shadowSplitRegex = /,(?![^(]*\))/;
|
|
// Regex to match individual parts of a box-shadow definition, ignoring spaces inside parentheses
|
|
const shadowPartsRegex = /(?:[^\s()]+|\([^)]*\))+/g;
|
|
// Regex to match valid length values (e.g., px, em, rem, %)
|
|
const lengthValueRegex = /^-?\d*\.?\d+(px|em|rem|%)?$/;
|
|
|
|
return input.split(shadowSplitRegex).map(shadow => {
|
|
shadow = shadow.trim();
|
|
const parts = shadow.match(shadowPartsRegex);
|
|
let x, y, blur, spread, color;
|
|
let lengthValues = [];
|
|
|
|
for (let i = 0; i < parts.length; i++) {
|
|
const part = parts[i];
|
|
|
|
if (lengthValueRegex.test(part)) {
|
|
lengthValues.push(part);
|
|
} else {
|
|
color = parts.slice(i).join(" ");
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (lengthValues.length < 2 || lengthValues.length > 4) {
|
|
throw new Error("Invalid box-shadow syntax");
|
|
}
|
|
|
|
[x, y, blur, spread] = lengthValues;
|
|
|
|
if (color) {
|
|
const colorParts = color.split(" ");
|
|
if (colorParts.includes("inset")) {
|
|
colorParts.splice(colorParts.indexOf("inset"), 1);
|
|
}
|
|
if (colorParts.includes("outset")) {
|
|
colorParts.splice(colorParts.indexOf("outset"), 1);
|
|
}
|
|
color = colorParts.length ? colorParts.join(" ") : undefined;
|
|
}
|
|
|
|
return {
|
|
x: x || DEFAULT_SHADOW.x,
|
|
y: y || DEFAULT_SHADOW.y,
|
|
blur: blur || DEFAULT_SHADOW.blur,
|
|
spread: spread || DEFAULT_SHADOW.spread,
|
|
color: color || DEFAULT_SHADOW.color,
|
|
};
|
|
});
|
|
}
|
|
|
|
// ---------
|
|
// Filter Functions
|
|
// ---------
|
|
|
|
/**
|
|
* Combines multiple filter functions into a single filter function.
|
|
*
|
|
* @param {...Function} filters - One or more filter functions to combine.
|
|
* @returns {Function} A new filter function that applies all provided filters.
|
|
*/
|
|
function mergeFilters(...filters) {
|
|
return token => {
|
|
for (const filter of filters) {
|
|
if (!filter(token)) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
};
|
|
}
|
|
|
|
function defaultFilter(token) {
|
|
// discard tokens starting with "font/"
|
|
if (token.path.includes("font")) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function filterBase(pathItem) {
|
|
return pathItem !== "@base";
|
|
}
|
|
|
|
// ---------
|
|
// Style Dictionary Configuration
|
|
// ---------
|
|
|
|
const platform = {
|
|
options: {
|
|
outputReferences: true,
|
|
showFileHeader: false,
|
|
},
|
|
transforms: ["name/figma", "attribute/figma"],
|
|
files: [
|
|
{
|
|
destination: "tokens-figma-colors.json",
|
|
format: "json/figma/colors",
|
|
},
|
|
{
|
|
destination: "tokens-figma-primitives.json",
|
|
format: "json/figma/primitives",
|
|
},
|
|
{
|
|
destination: "tokens-figma-theme.json",
|
|
format: "json/figma/theme",
|
|
},
|
|
],
|
|
};
|
|
|
|
module.exports = {
|
|
platform,
|
|
formats: {
|
|
"json/figma/colors": formatTokens(COLLECTIONS.colors),
|
|
"json/figma/primitives": formatTokens(COLLECTIONS.primitives),
|
|
"json/figma/theme": formatTokens(COLLECTIONS.theme),
|
|
},
|
|
};
|