Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Style dictionary tokens #433

Merged
merged 9 commits into from Oct 24, 2023
6 changes: 6 additions & 0 deletions build/props.js
Expand Up @@ -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

Expand Down Expand Up @@ -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')
Expand Down
160 changes: 160 additions & 0 deletions 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)
})
}, {})
}
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Expand Up @@ -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",
Expand Down