|
| 1 | +/* |
| 2 | + * Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. |
| 3 | + * |
| 4 | + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with |
| 5 | + * the License. A copy of the License is located at |
| 6 | + * |
| 7 | + * http://www.apache.org/licenses/LICENSE-2.0 |
| 8 | + * |
| 9 | + * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR |
| 10 | + * CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions |
| 11 | + * and limitations under the License. |
| 12 | + */ |
| 13 | +import type {Dictionary, FormattingOptions, OutputReferences, TransformedToken} from 'style-dictionary/types' |
| 14 | +import {getReferences, usesReferences} from 'style-dictionary/utils' |
| 15 | + |
| 16 | +/** |
| 17 | + * @typedef {import('../../../types/DesignToken.d.ts').TransformedToken} TransformedToken |
| 18 | + * @typedef {import('../../../types/DesignToken.d.ts').Dictionary} Dictionary |
| 19 | + * @typedef {import('../../../types/File.d.ts').FormattingOptions} Formatting |
| 20 | + * @typedef {import('../../../types/Format.d.ts').OutputReferences} OutputReferences |
| 21 | + */ |
| 22 | + |
| 23 | +/** |
| 24 | + * @type {Formatting} |
| 25 | + */ |
| 26 | +const defaultFormatting = { |
| 27 | + prefix: '', |
| 28 | + commentStyle: 'long', |
| 29 | + commentPosition: 'inline', |
| 30 | + indentation: '', |
| 31 | + separator: ' =', |
| 32 | + suffix: ';', |
| 33 | +} |
| 34 | + |
| 35 | +/** |
| 36 | + * Split a string comment by newlines and |
| 37 | + * convert to multi-line comment if necessary |
| 38 | + * @param {string} toRetToken |
| 39 | + * @param {string} comment |
| 40 | + * @param {Formatting} options |
| 41 | + * @returns {string} |
| 42 | + */ |
| 43 | +export function addComment(toRetToken: string, comment: string, options: FormattingOptions) { |
| 44 | + const {commentStyle, indentation} = options |
| 45 | + let {commentPosition} = options |
| 46 | + |
| 47 | + const commentsByNewLine = comment.split('\n') |
| 48 | + if (commentsByNewLine.length > 1) { |
| 49 | + commentPosition = 'above' |
| 50 | + } |
| 51 | + |
| 52 | + let processedComment |
| 53 | + switch (commentStyle) { |
| 54 | + case 'short': |
| 55 | + if (commentPosition === 'inline') { |
| 56 | + processedComment = `// ${comment}` |
| 57 | + } else { |
| 58 | + processedComment = commentsByNewLine.reduce((acc, curr) => `${acc}${indentation}// ${curr}\n`, '') |
| 59 | + // remove trailing newline |
| 60 | + processedComment = processedComment.replace(/\n$/g, '') |
| 61 | + } |
| 62 | + break |
| 63 | + case 'long': |
| 64 | + if (commentsByNewLine.length > 1) { |
| 65 | + processedComment = commentsByNewLine.reduce( |
| 66 | + (acc, curr) => `${acc}${indentation} * ${curr}\n`, |
| 67 | + `${indentation}/**\n`, |
| 68 | + ) |
| 69 | + processedComment += `${indentation} */` |
| 70 | + } else { |
| 71 | + processedComment = `${commentPosition === 'above' ? indentation : ''}/* ${comment} */` |
| 72 | + } |
| 73 | + break |
| 74 | + } |
| 75 | + |
| 76 | + if (commentPosition === 'above') { |
| 77 | + // put the comment above the token if it's multi-line or if commentStyle ended with -above |
| 78 | + toRetToken = `${processedComment}\n${toRetToken}` |
| 79 | + } else { |
| 80 | + toRetToken = `${toRetToken} ${processedComment}` |
| 81 | + } |
| 82 | + |
| 83 | + return toRetToken |
| 84 | +} |
| 85 | + |
| 86 | +/** |
| 87 | + * Creates a function that can be used to format a token. This can be useful |
| 88 | + * to use as the function on `dictionary.allTokens.map`. The formatting |
| 89 | + * is configurable either by supplying a `format` option or a `formatting` object |
| 90 | + * which uses: prefix, indentation, separator, suffix, and commentStyle. |
| 91 | + * @memberof module:formatHelpers |
| 92 | + * @name createPropertyFormatter |
| 93 | + * @example |
| 94 | + * ```javascript |
| 95 | + * import { propertyFormatNames } from 'style-dictionary/enums'; |
| 96 | + * |
| 97 | + * StyleDictionary.registerFormat({ |
| 98 | + * name: 'myCustomFormat', |
| 99 | + * format: function({ dictionary, options }) { |
| 100 | + * const { outputReferences } = options; |
| 101 | + * const formatProperty = createPropertyFormatter({ |
| 102 | + * outputReferences, |
| 103 | + * dictionary, |
| 104 | + * format: propertyFormatNames.css |
| 105 | + * }); |
| 106 | + * return dictionary.allTokens.map(formatProperty).join('\n'); |
| 107 | + * } |
| 108 | + * }); |
| 109 | + * ``` |
| 110 | + * @param {Object} options |
| 111 | + * @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. |
| 112 | + * @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. |
| 113 | + * @param {Dictionary} options.dictionary - The dictionary object sent to the format function |
| 114 | + * @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 |
| 115 | + * @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. |
| 116 | + * @param {boolean} [options.themeable] [false] - Whether tokens should default to being themeable. |
| 117 | + * @param {boolean} [options.usesDtcg] [false] - Whether DTCG token syntax should be uses. |
| 118 | + * @returns {(token: import('../../../types/DesignToken.d.ts').TransformedToken) => string} |
| 119 | + */ |
| 120 | +export default function createPropertyFormatterWithRef({ |
| 121 | + outputReferences = false, |
| 122 | + outputReferenceFallbacks = false, |
| 123 | + dictionary, |
| 124 | + format, |
| 125 | + formatting = {}, |
| 126 | + themeable = false, |
| 127 | + usesDtcg = false, |
| 128 | +}: { |
| 129 | + outputReferences?: OutputReferences |
| 130 | + outputReferenceFallbacks?: boolean |
| 131 | + dictionary: Dictionary |
| 132 | + format?: string |
| 133 | + formatting?: FormattingOptions |
| 134 | + themeable?: boolean |
| 135 | + usesDtcg?: boolean |
| 136 | +}) { |
| 137 | + /** @type {Formatting} */ |
| 138 | + const formatDefaults: FormattingOptions = {} |
| 139 | + switch (format) { |
| 140 | + case 'css': |
| 141 | + formatDefaults.prefix = '--' |
| 142 | + formatDefaults.indentation = ' ' |
| 143 | + formatDefaults.separator = ':' |
| 144 | + break |
| 145 | + } |
| 146 | + const mergedOptions = { |
| 147 | + ...defaultFormatting, |
| 148 | + ...formatDefaults, |
| 149 | + ...formatting, |
| 150 | + } |
| 151 | + const {prefix, commentStyle, indentation, separator, suffix} = mergedOptions |
| 152 | + const {tokens, unfilteredTokens} = dictionary |
| 153 | + return function (token: TransformedToken) { |
| 154 | + let toRetToken = `${indentation}${prefix}${token.name}${separator} ` |
| 155 | + let value = usesDtcg ? token.$value : token.value |
| 156 | + const originalValue = usesDtcg ? token.original.$value : token.original.value |
| 157 | + const shouldOutputRef = |
| 158 | + usesReferences(originalValue) && |
| 159 | + (typeof outputReferences === 'function' ? outputReferences(token, {dictionary, usesDtcg}) : outputReferences) |
| 160 | + /** |
| 161 | + * A single value can have multiple references either by interpolation: |
| 162 | + * "value": "{size.border.width.value} solid {color.border.primary.value}" |
| 163 | + * or if the value is an object: |
| 164 | + * "value": { |
| 165 | + * "size": "{size.border.width.value}", |
| 166 | + * "style": "solid", |
| 167 | + * "color": "{color.border.primary.value"} |
| 168 | + * } |
| 169 | + * This will see if there are references and if there are, replace |
| 170 | + * the resolved value with the reference's name. |
| 171 | + */ |
| 172 | + if (shouldOutputRef) { |
| 173 | + // Formats that use this function expect `value` to be a string |
| 174 | + // or else you will get '[object Object]' in the output |
| 175 | + const refs = getReferences(originalValue, tokens, {unfilteredTokens, warnImmediately: false}, []) |
| 176 | + // original can either be an object value, which requires transitive value transformation in web CSS formats |
| 177 | + // or a different (primitive) type, meaning it can be stringified. |
| 178 | + const originalIsObject = typeof originalValue === 'object' && originalValue !== null |
| 179 | + |
| 180 | + if (!originalIsObject) { |
| 181 | + // TODO: find a better way to deal with object-value tokens and outputting refs |
| 182 | + // e.g. perhaps it is safer not to output refs when the value is transformed to a non-object |
| 183 | + // for example for CSS-like formats we always flatten to e.g. strings |
| 184 | + |
| 185 | + // when original is object value, we replace value by matching ref.value and putting a var instead. |
| 186 | + // Due to the original.value being an object, it requires transformation, so undoing the transformation |
| 187 | + // by replacing value with original.value is not possible. (this is the early v3 approach to outputting refs) |
| 188 | + |
| 189 | + // when original is string value, we replace value by matching original.value and putting a var instead |
| 190 | + // this is more friendly to transitive transforms that transform the string values (v4 way of outputting refs) |
| 191 | + value = originalValue |
| 192 | + } else { |
| 193 | + if (token.$type === 'border') { |
| 194 | + const transformedValues = value.split(' ') |
| 195 | + value = ['width', 'style', 'color'] |
| 196 | + .map((prop, index) => { |
| 197 | + if ( |
| 198 | + originalValue[prop].startsWith('{') && |
| 199 | + refs.find(ref => ref.path.join('.') === originalValue[prop].replace(/[{}]/g, ''))?.isSource === true |
| 200 | + ) { |
| 201 | + return originalValue[prop] |
| 202 | + } |
| 203 | + return transformedValues[index] |
| 204 | + }) |
| 205 | + .join(' ') |
| 206 | + } |
| 207 | + } |
| 208 | + /* eslint-disable-next-line github/array-foreach */ |
| 209 | + refs.forEach(ref => { |
| 210 | + // value should be a string that contains the resolved reference |
| 211 | + // because Style Dictionary resolved this in the resolution step. |
| 212 | + // Here we are undoing that by replacing the value with |
| 213 | + // the reference's name |
| 214 | + if (Object.hasOwn(ref, `${usesDtcg ? '$' : ''}value`) && Object.hasOwn(ref, 'name')) { |
| 215 | + const refVal = usesDtcg ? ref.$value : ref.value |
| 216 | + const replaceFunc = () => { |
| 217 | + if (format === 'css') { |
| 218 | + if (outputReferenceFallbacks) { |
| 219 | + return `var(${prefix}${ref.name}, ${refVal})` |
| 220 | + } else { |
| 221 | + return `var(${prefix}${ref.name})` |
| 222 | + } |
| 223 | + } else { |
| 224 | + return `${prefix}${ref.name}` |
| 225 | + } |
| 226 | + } |
| 227 | + // TODO: add test |
| 228 | + // technically speaking a reference can be made to a number or boolean token, in this case we stringify it first |
| 229 | + const regex = new RegExp(`{${ref.path.join('\\.')}(\\.\\$?value)?}`, 'g') |
| 230 | + value = `${value}`.replace(regex, replaceFunc) |
| 231 | + } |
| 232 | + }) |
| 233 | + } |
| 234 | + toRetToken += value |
| 235 | + |
| 236 | + const themeableToken = typeof token.themeable === 'boolean' ? token.themeable : themeable |
| 237 | + if (format === 'sass' && themeableToken) { |
| 238 | + toRetToken += ' !default' |
| 239 | + } |
| 240 | + |
| 241 | + toRetToken += suffix |
| 242 | + const comment = token.$description ?? token.comment |
| 243 | + if (comment && commentStyle !== 'none') { |
| 244 | + toRetToken = addComment(toRetToken, comment, mergedOptions as FormattingOptions) |
| 245 | + } |
| 246 | + return toRetToken |
| 247 | + } |
| 248 | +} |
0 commit comments