diff --git a/.changeset/violet-lemons-greet.md b/.changeset/violet-lemons-greet.md new file mode 100644 index 000000000..fc56571d9 --- /dev/null +++ b/.changeset/violet-lemons-greet.md @@ -0,0 +1,5 @@ +--- +'@primer/primitives': minor +--- + +Enable token references in composite tokens diff --git a/scripts/buildTokens.ts b/scripts/buildTokens.ts index f5cf2c0af..febcf8579 100644 --- a/scripts/buildTokens.ts +++ b/scripts/buildTokens.ts @@ -1,4 +1,4 @@ -import type {Config} from 'style-dictionary/types' +import type {Config, LogConfig} from 'style-dictionary/types' import {PrimerStyleDictionary} from '../src/primerStyleDictionary.js' import {copyFromDir} from '../src/utilities/index.js' import {deprecatedJson, css, docJson, fallbacks, styleLint} from '../src/platforms/index.js' @@ -12,6 +12,14 @@ import {themes} from './themes.config.js' import fs from 'fs' import {getFallbackTheme} from './utilities/getFallbackTheme.js' +const log: LogConfig = { + warnings: 'disabled', // 'warn' | 'error' | 'disabled' + verbosity: 'silent', // 'default' | 'silent' | 'verbose' + errors: { + brokenReferences: 'throw', // 'throw' | 'console' + }, +} + /** * getStyleDictionaryConfig * @param filename output file name without extension @@ -29,13 +37,7 @@ const getStyleDictionaryConfig: StyleDictionaryConfigGenerator = ( ): Config => ({ source, // build the special formats include, - log: { - warnings: 'disabled', // 'warn' | 'error' | 'disabled' - verbosity: 'silent', // 'default' | 'silent' | 'verbose' - errors: { - brokenReferences: 'throw', // 'throw' | 'console' - }, - }, + log, platforms: Object.fromEntries( Object.entries({ css: css(`css/${filename}.css`, options.prefix, options.buildPath, { @@ -64,6 +66,7 @@ export const buildDesignTokens = async (buildOptions: ConfigGeneratorOptions): P const extendedSD = await PrimerStyleDictionary.extend({ source: [...source, ...include], // build the special formats include, + log, platforms: { css: css(`internalCss/${filename}.css`, buildOptions.prefix, buildOptions.buildPath, { themed: true, diff --git a/src/formats/cssAdvanced.ts b/src/formats/cssAdvanced.ts index 981f66277..11ac6cc2f 100644 --- a/src/formats/cssAdvanced.ts +++ b/src/formats/cssAdvanced.ts @@ -1,6 +1,7 @@ import type {TransformedToken, FormatFn, FormatFnArguments, FormattingOptions} from 'style-dictionary/types' import {format} from 'prettier' -import {fileHeader, formattedVariables, sortByName} from 'style-dictionary/utils' +import {fileHeader, sortByName} from 'style-dictionary/utils' +import getFormattedVariables from './utilities/getFormattedVariables.js' const wrapWithSelector = (css: string, selector: string | false): string => { // return without selector @@ -74,7 +75,7 @@ export const cssAdvanced: FormatFn = async ({ // early abort if no matches if (!filteredDictionary.allTokens.length) continue // add tokens into root - const css = formattedVariables({ + const css = getFormattedVariables({ format: 'css', dictionary: filteredDictionary, outputReferences, diff --git a/src/formats/utilities/createPropertyFormatterWithRef.ts b/src/formats/utilities/createPropertyFormatterWithRef.ts new file mode 100644 index 000000000..9a999aa85 --- /dev/null +++ b/src/formats/utilities/createPropertyFormatterWithRef.ts @@ -0,0 +1,251 @@ +/* + * 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 type {Dictionary, FormattingOptions, OutputReferences, TransformedToken} from 'style-dictionary/types' +import {getReferences, usesReferences} from 'style-dictionary/utils' +import {processCompositeTokenReferences} from './processCompositeTokenReferences.js' + +/** + * @typedef {import('../../../types/DesignToken.d.ts').TransformedToken} TransformedToken + * @typedef {import('../../../types/DesignToken.d.ts').Dictionary} Dictionary + * @typedef {import('../../../types/File.d.ts').FormattingOptions} Formatting + * @typedef {import('../../../types/Format.d.ts').OutputReferences} OutputReferences + */ + +/** + * @type {Formatting} + */ +const defaultFormatting = { + prefix: '', + commentStyle: 'long', + commentPosition: 'inline', + indentation: '', + separator: ' =', + suffix: ';', +} + +/** + * Split a string comment by newlines and + * convert to multi-line comment if necessary + * @param {string} toRetToken + * @param {string} comment + * @param {Formatting} options + * @returns {string} + */ +export function addComment(toRetToken: string, comment: string, options: FormattingOptions) { + const {commentStyle, indentation} = options + let {commentPosition} = options + + const commentsByNewLine = comment.split('\n') + if (commentsByNewLine.length > 1) { + commentPosition = 'above' + } + + let processedComment + switch (commentStyle) { + case 'short': + if (commentPosition === 'inline') { + processedComment = `// ${comment}` + } else { + processedComment = commentsByNewLine.reduce((acc, curr) => `${acc}${indentation}// ${curr}\n`, '') + // remove trailing newline + processedComment = processedComment.replace(/\n$/g, '') + } + break + case 'long': + if (commentsByNewLine.length > 1) { + processedComment = commentsByNewLine.reduce( + (acc, curr) => `${acc}${indentation} * ${curr}\n`, + `${indentation}/**\n`, + ) + processedComment += `${indentation} */` + } else { + processedComment = `${commentPosition === 'above' ? indentation : ''}/* ${comment} */` + } + break + } + + if (commentPosition === 'above') { + // put the comment above the token if it's multi-line or if commentStyle ended with -above + toRetToken = `${processedComment}\n${toRetToken}` + } else { + toRetToken = `${toRetToken} ${processedComment}` + } + + return toRetToken +} + +/** + * Creates a function that can be used to format a token. This can be useful + * to use as the function on `dictionary.allTokens.map`. The formatting + * is configurable either by supplying a `format` option or a `formatting` object + * which uses: prefix, indentation, separator, suffix, and commentStyle. + * @memberof module:formatHelpers + * @name createPropertyFormatter + * @example + * ```javascript + * import { propertyFormatNames } from 'style-dictionary/enums'; + * + * StyleDictionary.registerFormat({ + * name: 'myCustomFormat', + * format: function({ dictionary, options }) { + * const { outputReferences } = options; + * const formatProperty = createPropertyFormatter({ + * outputReferences, + * dictionary, + * format: propertyFormatNames.css + * }); + * return dictionary.allTokens.map(formatProperty).join('\n'); + * } + * }); + * ``` + * @param {Object} options + * @param {OutputReferences} [options.outputReferences] - Whether or not to output references. You will want to pass this from the `options` object sent to the format function. + * @param {boolean} [options.outputReferenceFallbacks] - Whether or not to output css variable fallback values when using output references. You will want to pass this from the `options` object sent to the format function. + * @param {Dictionary} options.dictionary - The dictionary object sent to the format function + * @param {string} [options.format] - Available formats are: 'css', 'sass', 'less', and 'stylus'. If you want to customize the format and can't use one of those predefined formats, use the `formatting` option + * @param {Formatting} [options.formatting] - Custom formatting properties that define parts of a declaration line in code. The configurable strings are: `prefix`, `indentation`, `separator`, `suffix`, `lineSeparator`, `fileHeaderTimestamp`, `header`, `footer`, `commentStyle` and `commentPosition`. Those are used to generate a line like this: `${indentation}${prefix}${token.name}${separator} ${prop.value}${suffix}`. The remaining formatting options are used for the fileHeader helper. + * @param {boolean} [options.themeable] [false] - Whether tokens should default to being themeable. + * @param {boolean} [options.usesDtcg] [false] - Whether DTCG token syntax should be uses. + * @returns {(token: import('../../../types/DesignToken.d.ts').TransformedToken) => string} + */ +export default function createPropertyFormatterWithRef({ + outputReferences = false, + outputReferenceFallbacks = false, + dictionary, + format, + formatting = {}, + themeable = false, + usesDtcg = false, +}: { + outputReferences?: OutputReferences + outputReferenceFallbacks?: boolean + dictionary: Dictionary + format?: string + formatting?: FormattingOptions + themeable?: boolean + usesDtcg?: boolean +}) { + /** @type {Formatting} */ + const formatDefaults: FormattingOptions = {} + switch (format) { + case 'css': + formatDefaults.prefix = '--' + formatDefaults.indentation = ' ' + formatDefaults.separator = ':' + break + } + const mergedOptions = { + ...defaultFormatting, + ...formatDefaults, + ...formatting, + } + const {prefix, commentStyle, indentation, separator, suffix} = mergedOptions + const {tokens, unfilteredTokens} = dictionary + return function (token: TransformedToken) { + let toRetToken = `${indentation}${prefix}${token.name}${separator} ` + let value = usesDtcg ? token.$value : token.value + const originalValue = usesDtcg ? token.original.$value : token.original.value + const shouldOutputRef = + usesReferences(originalValue) && + (typeof outputReferences === 'function' ? outputReferences(token, {dictionary, usesDtcg}) : outputReferences) + /** + * A single value can have multiple references either by interpolation: + * "value": "{size.border.width.value} solid {color.border.primary.value}" + * or if the value is an object: + * "value": { + * "size": "{size.border.width.value}", + * "style": "solid", + * "color": "{color.border.primary.value"} + * } + * This will see if there are references and if there are, replace + * the resolved value with the reference's name. + */ + if (shouldOutputRef) { + // Formats that use this function expect `value` to be a string + // or else you will get '[object Object]' in the output + const refs = getReferences(originalValue, tokens, {unfilteredTokens, warnImmediately: false}, []) + // original can either be an object value, which requires transitive value transformation in web CSS formats + // or a different (primitive) type, meaning it can be stringified. + const originalIsObject = typeof originalValue === 'object' && originalValue !== null + + if (!originalIsObject) { + // TODO: find a better way to deal with object-value tokens and outputting refs + // e.g. perhaps it is safer not to output refs when the value is transformed to a non-object + // for example for CSS-like formats we always flatten to e.g. strings + + // when original is object value, we replace value by matching ref.value and putting a var instead. + // Due to the original.value being an object, it requires transformation, so undoing the transformation + // by replacing value with original.value is not possible. (this is the early v3 approach to outputting refs) + + // when original is string value, we replace value by matching original.value and putting a var instead + // this is more friendly to transitive transforms that transform the string values (v4 way of outputting refs) + value = originalValue + } else { + if (token.$type === 'border') { + value = processCompositeTokenReferences(value, originalValue, ['width', 'style', 'color'], refs) + } + if (token.$type === 'shadow') { + value = processCompositeTokenReferences( + value, + originalValue, + ['offsetX', 'offsetY', 'blur', 'spread', 'color'], + refs, + ['inset'], + ) + } + // add if clause for transition tokens + if (token.$type === 'transition') { + value = processCompositeTokenReferences(value, originalValue, ['duration', 'timingFunction', 'delay'], refs) + } + } + /* eslint-disable-next-line github/array-foreach */ + refs.forEach(ref => { + // value should be a string that contains the resolved reference + // because Style Dictionary resolved this in the resolution step. + // Here we are undoing that by replacing the value with + // the reference's name + if (Object.hasOwn(ref, `${usesDtcg ? '$' : ''}value`) && Object.hasOwn(ref, 'name')) { + const refVal = usesDtcg ? ref.$value : ref.value + const replaceFunc = () => { + if (format === 'css') { + if (outputReferenceFallbacks) { + return `var(${prefix}${ref.name}, ${refVal})` + } else { + return `var(${prefix}${ref.name})` + } + } else { + return `${prefix}${ref.name}` + } + } + // TODO: add test + // technically speaking a reference can be made to a number or boolean token, in this case we stringify it first + const regex = new RegExp(`{${ref.path.join('\\.')}(\\.\\$?value)?}`, 'g') + value = `${value}`.replace(regex, replaceFunc) + } + }) + } + toRetToken += value + + const themeableToken = typeof token.themeable === 'boolean' ? token.themeable : themeable + if (format === 'sass' && themeableToken) { + toRetToken += ' !default' + } + + toRetToken += suffix + const comment = token.$description ?? token.comment + if (comment && commentStyle !== 'none') { + toRetToken = addComment(toRetToken, comment, mergedOptions as FormattingOptions) + } + return toRetToken + } +} diff --git a/src/formats/utilities/getFormattedVariables.ts b/src/formats/utilities/getFormattedVariables.ts new file mode 100644 index 000000000..e06b471d9 --- /dev/null +++ b/src/formats/utilities/getFormattedVariables.ts @@ -0,0 +1,74 @@ +/* + * 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 type {Dictionary, OutputReferences} from 'style-dictionary/types' +import {sortByReference} from 'style-dictionary/utils' +import createPropertyFormatterWithRef from './createPropertyFormatterWithRef.js' + +const defaultFormatting = { + lineSeparator: '\n', +} + +export default function getFormattedVariables({ + format, + dictionary, + outputReferences = false, + outputReferenceFallbacks, + formatting = {}, + themeable = false, + usesDtcg = false, +}: { + format: string + dictionary: Dictionary + outputReferences?: OutputReferences + outputReferenceFallbacks?: boolean + formatting?: { + lineSeparator?: string + } + themeable?: boolean + usesDtcg?: boolean +}) { + // typecast, we know that by know the tokens have been transformed + let allTokens = dictionary.allTokens + const tokens = dictionary.tokens + + const {lineSeparator} = Object.assign({}, defaultFormatting, formatting) + + // Some languages are imperative, meaning a variable has to be defined + // before it is used. If `outputReferences` is true, check if the token + // has a reference, and if it does send it to the end of the array. + // We also need to account for nested references, a -> b -> c. They + // need to be defined in reverse order: c, b, a so that the reference always + // comes after the definition + if (outputReferences) { + // note: using the spread operator here so we get a new array rather than + // mutating the original + allTokens = [...allTokens].sort(sortByReference(tokens, {unfilteredTokens: dictionary.unfilteredTokens, usesDtcg})) + } + + return allTokens + .map( + createPropertyFormatterWithRef({ + outputReferences, + outputReferenceFallbacks, + dictionary, + format, + formatting, + themeable, + usesDtcg, + }), + ) + .filter(function (strVal) { + return !!strVal + }) + .join(lineSeparator) +} diff --git a/src/formats/utilities/processCompositeTokenReferences.test.ts b/src/formats/utilities/processCompositeTokenReferences.test.ts new file mode 100644 index 000000000..9c71d438f --- /dev/null +++ b/src/formats/utilities/processCompositeTokenReferences.test.ts @@ -0,0 +1,169 @@ +import {processCompositeTokenReferences} from './processCompositeTokenReferences.js' + +describe('processCompositeTokenReferences', () => { + // ... existing code ... + + test('should handle composite border token references', () => { + const value = '1px solid #000000' + const originalValue = { + width: '{border.width.small}', + style: '{border.style.solid}', + color: '{color.black}', + } + const properties = ['width', 'style', 'color'] + const refs = [ + {path: ['border', 'width', 'small'], isSource: true}, + {path: ['border', 'style', 'solid'], isSource: true}, + {path: ['color', 'black'], isSource: true}, + ] + const sanitizeValue: string[] = [] + + const result = processCompositeTokenReferences(value, originalValue, properties, refs, sanitizeValue) + expect(result).toBe('{border.width.small} {border.style.solid} {color.black}') + }) + + test('should handle composite border token with mixed references', () => { + const value = '1px solid #000000' + const originalValue = { + width: '{border.width.small}', + style: 'solid', + color: '{color.black}', + } + const properties = ['width', 'style', 'color'] + const refs = [ + {path: ['border', 'width', 'small'], isSource: true}, + {path: ['color', 'black'], isSource: true}, + ] + const sanitizeValue = ['px'] + + const result = processCompositeTokenReferences(value, originalValue, properties, refs, sanitizeValue) + expect(result).toBe('{border.width.small} solid {color.black}') + }) + + test('should handle composite border token with invalid references', () => { + const value = '1px solid #000000' + const originalValue = { + width: '{border.width.small}', + style: '{border.style.solid}', + color: '{color.black}', + } + const properties = ['width', 'style', 'color'] + const refs = [ + {path: ['border', 'width', 'small'], isSource: false}, + {path: ['border', 'style', 'solid'], isSource: true}, + {path: ['color', 'black'], isSource: false}, + ] + const sanitizeValue = ['px'] + + const result = processCompositeTokenReferences(value, originalValue, properties, refs, sanitizeValue) + expect(result).toBe('1 {border.style.solid} #000000') + }) + + test('should handle composite shadow token references', () => { + const value = '0px 4px 6px -1px rgba(0, 0, 0, 0.1)' + const originalValue = { + offsetX: '{shadow.offsetX.none}', + offsetY: '{shadow.offsetY.small}', + blur: '{shadow.blur.medium}', + spread: '{shadow.spread.small}', + color: '{color.black.alpha10}', + } + const properties = ['offsetX', 'offsetY', 'blur', 'spread', 'color'] + const refs = [ + {path: ['shadow', 'offsetX', 'none'], isSource: true}, + {path: ['shadow', 'offsetY', 'small'], isSource: true}, + {path: ['shadow', 'blur', 'medium'], isSource: true}, + {path: ['shadow', 'spread', 'small'], isSource: true}, + {path: ['color', 'black', 'alpha10'], isSource: true}, + ] + const sanitizeValue: string[] = [] + + const result = processCompositeTokenReferences(value, originalValue, properties, refs, sanitizeValue) + expect(result).toBe( + '{shadow.offsetX.none} {shadow.offsetY.small} {shadow.blur.medium} {shadow.spread.small} {color.black.alpha10}', + ) + }) + + test('should handle composite shadow token with mixed references', () => { + const value = 'inset 0px 4px 6px -1px rgba(0, 0, 0, 0.1)' + const originalValue = { + inset: true, + offsetX: '{shadow.offsetX.none}', + offsetY: '4px', + blur: '{shadow.blur.medium}', + spread: '-1px', + color: '{color.black.alpha10}', + } + + const properties = ['offsetX', 'offsetY', 'blur', 'spread', 'color'] + const refs = [ + {path: ['shadow', 'offsetX', 'none'], isSource: true}, + {path: ['shadow', 'blur', 'medium'], isSource: true}, + {path: ['color', 'black', 'alpha10'], isSource: true}, + ] + const sanitizeValue = ['inset'] + + const result = processCompositeTokenReferences(value, originalValue, properties, refs, sanitizeValue) + expect(result).toBe('{shadow.offsetX.none} 4px {shadow.blur.medium} -1px {color.black.alpha10}') + }) + + test('should handle composite shadow token with invalid references', () => { + const value = '0px 4px 6px -1px rgba(0, 0, 0, 0.1)' + const originalValue = { + offsetX: '{shadow.offsetX.none}', + offsetY: '{shadow.offsetY.small}', + blur: '{shadow.blur.medium}', + spread: '{shadow.spread.small}', + color: '{color.black.alpha10}', + } + const properties = ['offsetX', 'offsetY', 'blur', 'spread', 'color'] + const refs = [ + {path: ['shadow', 'offsetX', 'none'], isSource: false}, + {path: ['shadow', 'offsetY', 'small'], isSource: true}, + {path: ['shadow', 'blur', 'medium'], isSource: false}, + {path: ['shadow', 'spread', 'small'], isSource: true}, + {path: ['color', 'black', 'alpha10'], isSource: false}, + ] + + const result = processCompositeTokenReferences(value, originalValue, properties, refs) + expect(result).toBe('0px {shadow.offsetY.small} 6px {shadow.spread.small} rgba(0, 0, 0, 0.1)') + }) + + test('should handle complex shadow token with multiple values', () => { + const value = '0px 4px 6px -1px rgba(0, 0, 0, 0.1), 0px 2px 4px -1px rgba(0, 0, 0, 0.06)' + const originalValue = { + shadow1: '{shadow.medium}', + shadow2: '{shadow.small}', + } + const properties = ['shadow1', 'shadow2'] + const refs = [ + {path: ['shadow', 'medium'], isSource: true}, + {path: ['shadow', 'small'], isSource: true}, + ] + + const result = processCompositeTokenReferences(value, originalValue, properties, refs) + expect(result).toBe('{shadow.medium} {shadow.small}') + }) + + test('should not split rgb or rgba values', () => { + const value = '0px 4px 6px -1px rgb(0, 0, 0)' + const originalValue = { + offsetX: '{shadow.offsetX.none}', + offsetY: '{shadow.offsetY.small}', + blur: '{shadow.blur.medium}', + spread: '{shadow.spread.small}', + color: '{color.black}', + } + const properties = ['offsetX', 'offsetY', 'blur', 'spread', 'color'] + const refs = [ + {path: ['shadow', 'offsetX', 'none'], isSource: false}, + {path: ['shadow', 'offsetY', 'small'], isSource: true}, + {path: ['shadow', 'blur', 'medium'], isSource: false}, + {path: ['shadow', 'spread', 'small'], isSource: true}, + {path: ['color', 'black'], isSource: false}, + ] + + const result = processCompositeTokenReferences(value, originalValue, properties, refs) + expect(result).toBe('0px {shadow.offsetY.small} 6px {shadow.spread.small} rgb(0, 0, 0)') + }) +}) diff --git a/src/formats/utilities/processCompositeTokenReferences.ts b/src/formats/utilities/processCompositeTokenReferences.ts new file mode 100644 index 000000000..73f562f0d --- /dev/null +++ b/src/formats/utilities/processCompositeTokenReferences.ts @@ -0,0 +1,50 @@ +export function processCompositeTokenReferences( + value: string, + originalValue: Record, + properties: string[], + refs: Array<{path: string[]; isSource: boolean}>, + sanitizeValue: string[] = [], +): string { + // Copy the value to avoid mutating it + const sanitizedValue = removeStringsFromString(value, sanitizeValue) // Remove the sanitized value from the original value + const transformedValues = sanitizedValue.split(/(? { + if ( + typeof originalValue[prop] === 'string' && + originalValue[prop].startsWith('{') && // Check if the original value is a reference + refs.find(ref => ref.path.join('.') === (originalValue[prop] as string).replace(/[{}]/g, ''))?.isSource === true // Validate the reference + ) { + return originalValue[prop] // Return the valid reference + } + return transformedValues[index] // Otherwise, return the default value + }) + .join(' ') // Join the processed values back into a single string +} + +/** + * Removes any string from the given array from the input string. + * + * @param input - The input string to process. + * @param stringsToRemove - An array of strings to remove from the input string. + * @returns The resulting string with the specified strings removed. + */ +export function removeStringsFromString(input: string, stringsToRemove: string[]): string { + // Create a regex pattern to match any of the strings in the array + const pattern = new RegExp(stringsToRemove.map(str => escapeRegExp(str)).join('|'), 'g') + + // Replace the matching strings with an empty string + return input.replace(pattern, '').trim() +} + +/** + * Escapes special characters in a string for use in a regular expression. + * + * @param str - The string to escape. + * @returns The escaped string. + */ +function escapeRegExp(str: string): string { + return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // Escapes special regex characters +} diff --git a/src/platforms/css.ts b/src/platforms/css.ts index 71af32d2a..7c3600327 100644 --- a/src/platforms/css.ts +++ b/src/platforms/css.ts @@ -2,6 +2,8 @@ import {isFromFile, isSource} from '../filters/index.js' import type {PlatformInitializer} from '../types/platformInitializer.js' import type {PlatformConfig, TransformedToken} from 'style-dictionary/types' import {outputReferencesTransformed, outputReferencesFilter} from 'style-dictionary/utils' +import {outputReferencesTransformedWithObject} from './utilities/outputReferencesTransformedWithObject.js' +import {outputReferencesFilterObject} from './utilities/outputReferencesFilterObject.js' const getCssSelectors = (outputFile: string) => { // check for dark in the beginning of the output filename @@ -58,8 +60,13 @@ export const css: PlatformInitializer = (outputFile, prefix, buildPath, options) ]), options: { showFileHeader: false, - outputReferences: (token, platformOptions) => - outputReferencesFilter(token, platformOptions) && outputReferencesTransformed(token, platformOptions), + // outputReferences: true, + outputReferences: (token, platformOptions) => { + return ( + outputReferencesFilterObject(token, platformOptions) && + outputReferencesTransformedWithObject(token, platformOptions) + ) + }, descriptions: false, queries: getCssSelectors(outputFile), ...options?.options, diff --git a/src/platforms/utilities/outputReferencesFilterObject.ts b/src/platforms/utilities/outputReferencesFilterObject.ts new file mode 100644 index 000000000..c98df5b64 --- /dev/null +++ b/src/platforms/utilities/outputReferencesFilterObject.ts @@ -0,0 +1,36 @@ +// const FILTER_WARNINGS = GroupMessages.GROUP.FilteredOutputReferences + +import type {Dictionary, TransformedToken} from 'style-dictionary/types' +import {getReferences} from 'style-dictionary/utils' + +/** + * @typedef {import('../../../types/DesignToken.d.ts').TransformedToken} TransformedToken + * @typedef {import('../../../types/DesignToken.d.ts').Dictionary} Dictionary + * + * @param {TransformedToken} token + * @param {{ dictionary: Dictionary, usesDtcg?: boolean }} dictionary + * @returns + */ +export function outputReferencesFilterObject( + token: TransformedToken, + {dictionary, usesDtcg}: {dictionary: Dictionary; usesDtcg?: boolean}, +): boolean { + const originalValue = usesDtcg ? token.original.$value : token.original.value + // get refs, pass unfilteredTokens to ensure we find the refs even if they are filtered out + const refs = getReferences(originalValue, dictionary.tokens, { + unfilteredTokens: dictionary.unfilteredTokens, + usesDtcg, + warnImmediately: false, + }) + + return refs.some(ref => { + // check whether every ref can be found in the filtered set of tokens + const foundToken = dictionary.allTokens.find(thisToken => thisToken.name === ref.name) + if (!foundToken) { + // remove the warning about this ref being filtered out, since we now prevent it from outputting it as a ref + // GroupMessages.remove(FILTER_WARNINGS, ref.path.join('.')) + } + + return !!foundToken + }) +} diff --git a/src/platforms/utilities/outputReferencesTransformedWithObject.ts b/src/platforms/utilities/outputReferencesTransformedWithObject.ts new file mode 100644 index 000000000..e0a601a40 --- /dev/null +++ b/src/platforms/utilities/outputReferencesTransformedWithObject.ts @@ -0,0 +1,38 @@ +import type {Dictionary, TransformedToken} from 'style-dictionary/types' +import {resolveReferences} from 'style-dictionary/utils' + +export function outputReferencesTransformedWithObject( + token: TransformedToken, + {dictionary, usesDtcg}: {dictionary: Dictionary; usesDtcg?: boolean}, +): boolean { + const originalValue = usesDtcg ? token.original.$value : token.original.value + const value = usesDtcg ? token.$value : token.value + + // double check if this is a string, technically speaking the token could also be an object + // and pass the usesReferences check + if (typeof originalValue === 'string') { + // Check if the token's value is the same as if we were resolve references on the original value + // This checks whether the token's value has been transformed e.g. transitive transforms. + // If it has been, that means we should not be outputting refs because this would undo the work of those transforms. + return ( + value === + resolveReferences(originalValue, dictionary.unfilteredTokens ?? dictionary.tokens, { + usesDtcg, + warnImmediately: false, + }) + ) + } + if (typeof originalValue === 'object') { + const originalValues = Object.values(originalValue).filter(val => typeof val === 'string') + + return originalValues.some(origVal => { + const resolvedValue = resolveReferences(origVal, dictionary.unfilteredTokens ?? dictionary.tokens, { + usesDtcg, + warnImmediately: false, + }) + + return typeof resolvedValue === 'string' ? value.split(' ').includes(resolvedValue) : false + }) + } + return false +} diff --git a/src/tokens/functional/color/bgColor.json5 b/src/tokens/functional/color/bgColor.json5 index 759171c52..435a42186 100644 --- a/src/tokens/functional/color/bgColor.json5 +++ b/src/tokens/functional/color/bgColor.json5 @@ -14,7 +14,7 @@ }, 'org.primer.overrides': { dark: '{base.color.neutral.1}', - "dark-high-contrast": "{base.color.neutral.0}", + 'dark-high-contrast': '{base.color.neutral.0}', }, }, }, @@ -32,8 +32,8 @@ }, 'org.primer.overrides': { dark: '{base.color.neutral.2}', - "light-high-contrast": "{base.color.neutral.3}", - "dark-high-contrast": "{base.color.neutral.2}", + 'light-high-contrast': '{base.color.neutral.3}', + 'dark-high-contrast': '{base.color.neutral.2}', }, }, }, @@ -51,7 +51,7 @@ }, 'org.primer.overrides': { dark: '{base.color.neutral.0}', - "light-high-contrast": "{base.color.neutral.2}", + 'light-high-contrast': '{base.color.neutral.2}', }, }, }, @@ -125,8 +125,8 @@ }, 'org.primer.overrides': { dark: '{base.color.neutral.3}', - "light-high-contrast": "{base.color.neutral.4}", - "dark-high-contrast": "{base.color.neutral.4}", + 'light-high-contrast': '{base.color.neutral.4}', + 'dark-high-contrast': '{base.color.neutral.4}', }, }, }, @@ -163,8 +163,8 @@ $value: '{base.color.neutral.8}', alpha: 0.2, }, - "light-high-contrast": "{base.color.neutral.4}", - "dark-high-contrast": "{base.color.neutral.3}", + 'light-high-contrast': '{base.color.neutral.4}', + 'dark-high-contrast': '{base.color.neutral.3}', }, }, }, @@ -179,8 +179,8 @@ }, 'org.primer.overrides': { dark: '{base.color.neutral.8}', - "light-high-contrast": "{base.color.neutral.10}", - "dark-high-contrast": "{base.color.neutral.7}", + 'light-high-contrast': '{base.color.neutral.10}', + 'dark-high-contrast': '{base.color.neutral.7}', }, }, }, @@ -220,7 +220,7 @@ }, 'org.primer.overrides': { dark: '{base.color.blue.5}', - "dark-high-contrast": "{base.color.blue.9}", + 'dark-high-contrast': '{base.color.blue.9}', }, }, }, @@ -249,11 +249,11 @@ alpha: 0.15, }, 'light-protanopia-deuteranopia': '{base.color.blue.0}', + 'dark-protanopia-deuteranopia': { $value: '{base.color.blue.4}', alpha: 0.2, }, - }, }, }, @@ -275,8 +275,8 @@ 'dark-tritanopia': '{base.color.blue.5}', 'light-protanopia-deuteranopia': '{base.color.blue.5}', 'dark-protanopia-deuteranopia': '{base.color.blue.5}', - "light-high-contrast": "{base.color.green.5}", - "dark-high-contrast": "{base.color.green.9}", + 'light-high-contrast': '{base.color.green.5}', + 'dark-high-contrast': '{base.color.green.9}', }, }, }, @@ -363,7 +363,7 @@ }, }, 'org.primer.overrides': { - "dark-high-contrast": "{base.color.yellow.9}", + 'dark-high-contrast': '{base.color.yellow.9}', }, }, }, @@ -409,7 +409,7 @@ 'org.primer.overrides': { 'light-tritanopia': '{base.color.red.5}', 'dark-tritanopia': '{base.color.red.5}', - "dark-high-contrast": "{base.color.orange.9}", + 'dark-high-contrast': '{base.color.orange.9}', }, }, }, @@ -454,8 +454,8 @@ }, 'org.primer.overrides': { 'light-protanopia-deuteranopia': '{base.color.orange.5}', - "dark-protanopia-deuteranopia": "{base.color.orange.5}", - "dark-high-contrast": "{base.color.red.9}", + 'dark-protanopia-deuteranopia': '{base.color.orange.5}', + 'dark-high-contrast': '{base.color.red.9}', }, }, }, @@ -542,7 +542,7 @@ }, }, 'org.primer.overrides': { - "dark-high-contrast": "{base.color.purple.9}", + 'dark-high-contrast': '{base.color.purple.9}', }, }, }, @@ -615,7 +615,7 @@ }, }, 'org.primer.overrides': { -"dark-high-contrast": "{base.color.pink.9}", + 'dark-high-contrast': '{base.color.pink.9}', }, }, },