// import src files from the tokens pkg folder import { tokens as _tokens } from '../src' import { DVR, MAIN_DVR, DEPRECATED_DVR, DEPRECATED_DVRS } from '../constant' import { processColorString } from './processColorString' const deprecated_color_names = [ 'blue', 'primary', 'blue__alt', 'blue--light', 'blue--light__alt', 'blue--lightest', 'blue--lightest__alt', 'primary-hue-04', 'primary-hue-01', 'red', 'red__alt', 'white', 'grey', 'grey--light', 'black', 'shadow', ].map((name) => `color.$${name}`) type Token = { value: string | number reference?: string isDeprecated?: boolean isReference?: boolean isBase?: boolean } type NestedToken = { [key: string]: Token | NestedToken } // type definition for the tokens object const tokens = _tokens as NestedToken function isToken(obj: Token | NestedToken): obj is Token { return 'value' in obj } /** * Checks if a given string is a valid hexadecimal color code. * * A valid hexadecimal color code starts with a '#' followed by exactly six characters, * which can be digits from 0 to 9 or letters from a to f (case insensitive). * * @param {any} color - Anything to be tested as a hexadecimal color code string. * @returns {boolean} Returns `true` if the string is a valid hex color code, otherwise `false`. */ export function isHexColor(color: any): color is string { if (typeof color !== 'string') { return false } // Updated regex to match either three or six characters after the '#' return /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(color) } /** * helper fn: escape special characters, e.g., '.' '.$' * @param {string} string * @returns {string} Returns the escaped string. */ function escapeRegExp(string: string) { return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') } /** * Helper function to split a path string into parts. * @param {string} path * @returns {string[]} Returns an array of path parts. */ function getPathParts(path: string): string[] { const escapedDeprecatedDVR = escapeRegExp(DEPRECATED_DVR) // . -> \. const escapedDeprecatedDVRs = DEPRECATED_DVRS.map(escapeRegExp).join('|') // .$ -> \.\$ const escapedMainDVR = escapeRegExp(MAIN_DVR) // .$ -> \.\$ // split the path into parts. If the path contains DVR, split by DVR, otherwise split by '.', to support the old format const pathParts = path.split( // make sure to escape the DVR characters // place escaped '.$' and escaped '.' before '-' to support references like palette.$primary-c100 new RegExp(`${escapedDeprecatedDVR}|${escapedDeprecatedDVRs}|${escapedMainDVR}|${DVR}`) ) return pathParts } /** * get obj in the token tree by path * Helper function to navigate through a nested token structure based on a given path. * Helper for the token function and isDeprecatedToken function. * @param {string} path - The separated path string used to navigate through the nested token structure. * @returns {Token | NestedToken} Returns the token or nested token object found at the specified path. */ function getObjByPath(path: string): Token | NestedToken { const pathParts = getPathParts(path) let currentStructure: Token | NestedToken = tokens // loop through the path parts for (const pathPart of pathParts) { if (currentStructure && typeof currentStructure === 'object' && pathPart in currentStructure) { currentStructure = (currentStructure as NestedToken)[pathPart] } else { return {} } } return currentStructure } //#region isBaseToken fn /** * Checks if a token specified by a path is a base token. eg. fontsize, palette * * @param {string} path - The dot-separated path string used to navigate through the nested token structure. * @returns {boolean} Returns `true` if the token found at the specified path is a base token, otherwise returns `false`. * If the path is invalid or does not lead to a token, the function also returns `false`. */ export function isBaseToken(path: string) { const currentStructure = getObjByPath(path) if (isToken(currentStructure) && 'isBase' in currentStructure) { return currentStructure.isBase as boolean } return false // return fallback if no suitable value is found } //#region isDeprecatedToken fn /** * Checks if a token specified by a path is deprecated. * * This function navigates through a nested structure of tokens based on the given path. * The path is a string of property names separated by dots, used to traverse the nested * token structure. If the specified path leads to a token, the function checks if this token * is marked as deprecated. * * @param {string} path - The dot-separated path string used to navigate through the nested token structure. * @returns {boolean} Returns `true` if the token found at the specified path is deprecated, otherwise returns `false`. * If the path is invalid or does not lead to a token, the function also returns `false`. */ export function isDeprecatedToken(path: string) { if (deprecated_color_names.includes(path)) { return true } const INVALID_PATH = ['Invalid path', 'color.$color'] if (INVALID_PATH.includes(path)) { return true } const currentStructure = getObjByPath(path) if (isToken(currentStructure) && 'isDeprecated' in currentStructure) { return currentStructure.isDeprecated as boolean } return false // return fallback if no suitable value is found } //#region token function /** * Get a token from the JSON files * @param path - The dot-separated path to the token (e.g., "color.text.default") * @param fallback - The fallback value to use if the token isn't found * @param globalTheme - The global theme to use (e.g., 'light' or 'dark') * @param isShallow - If true, only return the shallow reference, eg. 'palette.danger' * @returns The token value or the fallback value */ export function token(path: string, fallback: string = '', globalTheme: string = 'light', isShallow = false): string { const obj = getObjByPath(path) //#region reference // check if the obj has a reference key if (isToken(obj) && obj.isReference) { // check if we just need the shallow reference if (isShallow) { return obj.value.toString() } // handle reference tokens, eg color.text.danger -> palette.danger const referencePath = obj.value // handle complicated, eg '0 0 4px 0 palette.$neutral-c220' if (typeof referencePath === 'string' && referencePath.includes(' ')) { const parts = referencePath.split(' ') return parts .map((part) => { if ( part.includes(DVR) || part.includes(MAIN_DVR) || part.includes(DEPRECATED_DVR) || DEPRECATED_DVRS.some((dvr) => part.includes(dvr)) ) { return token(part, fallback, globalTheme) } return part }) .join(' ') } if (typeof referencePath !== 'string') return fallback // recursively call the token function with the reference path return token(referencePath, fallback, globalTheme) } //#endregion reference //#region value // check if the obj has a value key if (isToken(obj) && !obj.isReference) { const value = obj.value if (!isHexColor(value) && typeof value === 'string') { return processColorString(value) } else if (typeof value === 'string') { return value } else if (typeof value === 'number') { return value.toString() } else { return fallback } } //#endregion value //#region default // check if there is a default key in the obj if (typeof obj === 'object' && 'default' in obj) { const defaultStructure = obj['default'] if (!isToken(defaultStructure)) { return fallback } const { value } = defaultStructure // check reference if ('isReference' in defaultStructure) { const referencePath = defaultStructure['value'] if (typeof referencePath !== 'string') return fallback // recursively call the token function with the reference path return token(referencePath, fallback, globalTheme) } if (!isHexColor(value) && typeof value === 'string') { return processColorString(value) } if (typeof value !== 'string') { return fallback } return value } //#endregion default return fallback // return fallback if no suitable value is found } export default token