UNPKG

style-dictionary

Version:

Style once, use everywhere. A build system for creating cross-platform styles.

266 lines (245 loc) 10.7 kB
/* * 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 { getReferences } from '../../utils/references/getReferences.js'; import usesReferences from '../../utils/references/usesReferences.js'; import { commentStyles, commentPositions, propertyFormatNames } from '../../enums/index.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 */ const { short, long, none } = commentStyles; const { above, inline } = commentPositions; const { css, sass, less, stylus } = propertyFormatNames; /** * @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} to_ret_token * @param {string} comment * @param {Formatting} options * @returns {string} */ export function addComment(to_ret_token, comment, options) { 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 to_ret_token = `${processedComment}\n${to_ret_token}`; } else { to_ret_token = `${to_ret_token} ${processedComment}`; } return to_ret_token; } /** * 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} ${token.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 createPropertyFormatter({ outputReferences = false, outputReferenceFallbacks = false, dictionary, format, formatting = {}, themeable = false, usesDtcg = false, }) { /** @type {Formatting} */ const formatDefaults = {}; switch (format) { case css: formatDefaults.prefix = '--'; formatDefaults.indentation = ' '; formatDefaults.separator = ':'; break; case sass: formatDefaults.prefix = '$'; formatDefaults.commentStyle = short; formatDefaults.indentation = ''; formatDefaults.separator = ':'; break; case less: formatDefaults.prefix = '@'; formatDefaults.commentStyle = short; formatDefaults.indentation = ''; formatDefaults.separator = ':'; break; case stylus: formatDefaults.prefix = '$'; formatDefaults.commentStyle = short; formatDefaults.indentation = ''; formatDefaults.separator = '='; break; } const mergedOptions = { ...defaultFormatting, ...formatDefaults, ...formatting, }; let { prefix, commentStyle, indentation, separator, suffix } = mergedOptions; const { tokens, unfilteredTokens } = dictionary; return function (token) { let to_ret_token = `${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} solid {color.border.primary}" * or if the value is an object: * "value": { * "size": "{size.border.width}", * "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; } 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 = function () { 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 value = `${value}`.replace( originalIsObject ? refVal : new RegExp(`{${ref.path.join('\\.')}(\\.\\$?value)?}`, 'g'), replaceFunc, ); } }); } to_ret_token += value; const themeable_token = typeof token.themeable === 'boolean' ? token.themeable : themeable; if (format === sass && themeable_token) { to_ret_token += ' !default'; } to_ret_token += suffix; const comment = token.$description ?? token.comment; if (comment && commentStyle !== none) { to_ret_token = addComment(to_ret_token, comment, mergedOptions); } return to_ret_token; }; }