diff --git a/build/props.js b/build/props.js index fc8a4f06..c8f74e25 100644 --- a/build/props.js +++ b/build/props.js @@ -22,6 +22,7 @@ import {buildPropsStylesheet} from './to-stylesheet.js' import {toTokens} from './to-tokens.js' import {toObject} from './to-object.js' import {toFigmaTokens} from './to-figmatokens.js' +import {toStyleDictionary} from './to-style-dictionary.js' const [,,prefix='',useWhere,customSubject='',filePrefix=''] = process.argv @@ -77,6 +78,11 @@ const designtokens = toTokens(jsonbundle) const JSONtokens = fs.createWriteStream('../open-props.tokens.json') JSONtokens.end(JSON.stringify(Object.fromEntries(designtokens), null, 2)) +// gen style-dictionary tokens +const styledictionarytokens = toStyleDictionary(jsonbundle) +const StyleDictionaryTokens = fs.createWriteStream('../open-props.style-dictionary-tokens.json') +StyleDictionaryTokens.end(JSON.stringify(styledictionarytokens, null, 2)) + // gen figma tokens const figmatokens = toFigmaTokens(jsonbundle) const FigmaTokens = fs.createWriteStream('../open-props.figma-tokens.json') diff --git a/build/to-style-dictionary.js b/build/to-style-dictionary.js new file mode 100644 index 00000000..875aa319 --- /dev/null +++ b/build/to-style-dictionary.js @@ -0,0 +1,160 @@ +import * as Colors from '../src/props.colors.js' + +// Mapping of CSS variable names to dictionary keys +const dictionaryMap = { + "size-relative": "relative", + "size-fluid": "fluid", + "size-header": "header", + "size-content": "content", + "border-size": "size", + "radius-conditional": "conditional", + "radius-blob": "blob" +} + +// Map a value to a dictionary key using the dictionaryMap +const mapToDictionaryKey = (value) => dictionaryMap[value] || value + +// Determine the type key based on the metaType +const getTypeKey = (metaType) => { + if (metaType === "size" || metaType === "border-radius") { + return metaType === "size" ? "size" : "radius" + } else if (metaType === "border-width") { + return "border" + } + return metaType +} + +// Count the occurrences of a character in a string +const countOccurrences = (str, letter) => (str.match(new RegExp(letter, 'g')) || []).length + +// Regular expression to match CSS variable usages +const cssVarUsageRegex = /var\(--([a-zA-Z0-9-]+)\)/g + +// Replace the last occurrence of a pattern with a replacement +/* https://www.30secondsofcode.org/js/s/replace-last-occurrence/ */ +const replaceLast = (str, pattern, replacement) => { + const match = + typeof pattern === 'string' + ? pattern + : (str.match(new RegExp(pattern.source, 'g')) || []).slice(-1)[0] + if (!match) return str + const last = str.lastIndexOf(match) + return last !== -1 + ? `${str.slice(0, last)}${replacement}${str.slice(last + match.length)}` + : str +} + +// Helper function to convert CSS variable name to token reference +const tokenizeCSSVar = (variableName, metaType) => { + const tokenName = replaceLast(variableName, '-', '.') + const hyphenCount = countOccurrences(variableName, '-') + + if (hyphenCount > 2 && metaType === "other") { + const [firstPart, ...restParts] = tokenName.split('-') + return `{${metaType}.${firstPart}.${restParts.join('-')}.value}` + } + + return `{${tokenName}.value}` +}; + +// Convert CSS variable usages to token references +const cssVarToToken = (input, metaType) => { + if (!input.toString().includes("var")) { + return input + } + + return input.replace(cssVarUsageRegex, (match, variableName) => { + return tokenizeCSSVar(variableName, metaType) + }) +}; + +// Create a token object based on metaType and dictionary key +const createTokenObject = ({ + baseObj, + mainKey, + metaType, + dictionarykey, + index, + token +}) => { + const typeKey = getTypeKey(metaType) + const targetObj = baseObj[typeKey] = baseObj[typeKey] || {} + + if (typeKey === "size" || typeKey === "radius") { + const shouldReplace = mainKey !== dictionarykey + handleKey(targetObj, dictionarykey, index, token, metaType, shouldReplace) + } else if (typeKey !== "other") { + handleKey(targetObj, dictionarykey, index, token, metaType, true) + } else { + handleOtherTypes(targetObj, dictionarykey, index, token, metaType) + } + + return baseObj +} + +// Handle cases where meta.type != "other" +function handleKey(targetObj, dictionarykey, index, token, metaType, shouldReplace) { + if (shouldReplace) { + targetObj[dictionarykey] = targetObj[dictionarykey] || {} + targetObj[dictionarykey][index] = { value: token, type: metaType } + } else { + targetObj[index] = { value: token, type: metaType } + } +} + +// Handle cases where meta.type = "other" +function handleOtherTypes(targetObj, dictionarykey, index, token, metaType) { + const keyParts = dictionarykey.split("-") + + if (keyParts.length > 1) { + const groupName = keyParts[0] + targetObj[groupName] = targetObj[groupName] || {} + targetObj[groupName][index] = { value: token, type: metaType } + + const rest = keyParts.slice(1) + const subKey = rest.join("-") + + targetObj[groupName][subKey] = targetObj[groupName][subKey] || {} + targetObj[groupName][subKey][index] = { value: token, type: metaType } + } +} + +// Generate a style dictionary +export const toStyleDictionary = props => { + const colors = Object.keys(Colors) + .filter(exportName => exportName !== "default") + .map(hueName => hueName.toLowerCase()) + + return props.reduce((styledictionarytokens, [key, token]) => { + const meta = {} + + const isLength = key.includes('size') && !key.includes('border-size') + const isBorder = key.includes('border-size') + const isRadius = key.includes('radius') + const isShadow = key.includes('shadow') + const isColor = colors.some(color => key.includes(color)) + + if (isLength) meta.type = 'size' + else if (isBorder) meta.type = 'border-width' + else if (isRadius) meta.type = 'border-radius' + else if (isShadow) meta.type = 'box-shadow' + else if (isColor) meta.type = 'color' + else meta.type = 'other' + + const keyWithoutPrefix = key.replace('--', '') + const keyParts = keyWithoutPrefix.split('-') + const mainKey = keyParts.length > 1 ? keyParts.slice(0, -1).join('-') : keyParts[0] + const index = keyParts.length > 1 ? keyParts[keyParts.length - 1] : 0 + + const dictionarykey = mapToDictionaryKey(mainKey) + + return createTokenObject({ + baseObj: styledictionarytokens, + mainKey, + metaType: meta.type, + dictionarykey, + index, + token: cssVarToToken(token, meta.type) + }) + }, {}) +} diff --git a/package-lock.json b/package-lock.json index 4e0659ae..1efa6ab6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "open-props", - "version": "1.5.16", + "version": "1.6.8", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "open-props", - "version": "1.5.16", + "version": "1.6.8", "license": "MIT", "devDependencies": { "ava": "^3.15.0", diff --git a/package.json b/package.json index eca9bf84..64a78e94 100644 --- a/package.json +++ b/package.json @@ -171,6 +171,7 @@ "./json": "./open-props.tokens.json", "./tokens": "./open-props.tokens.json", "./design-tokens": "./open-props.tokens.json", + "./style-dictionary-tokens": "./open-props.style-dictionary-tokens.json", "./postcss/brand": "./src/extra/brand.css", "./postcss/theme": "./src/extra/theme.css", "./postcss/theme-dark": "./src/extra/theme-dark.css",