UNPKG

@patternfly/react-tokens

Version:

This library provides access to the design tokens of PatternFly 4 from JavaScript

254 lines (227 loc) 8.86 kB
import { glob } from 'glob'; import { createRequire } from 'node:module'; import { dirname, basename, sep } from 'node:path'; import { parse, stringify } from 'css'; import { readFileSync } from 'node:fs'; const require = createRequire(import.meta.url); const pfStylesDir = dirname(require.resolve('@patternfly/patternfly/patternfly.css')); const version = 'v6'; // Helpers const formatCustomPropertyName = (key) => key.replace(`--pf-${version}-`, '').replace('--pf-t--', 't_').replace(/-+/g, '_'); const getRegexMatches = (string, regex) => { const res = {}; let matches; while ((matches = regex.exec(string))) { res[matches[1]] = matches[2].trim(); } return res; }; const getDeclarations = (cssAst) => cssAst.stylesheet.rules .filter( (node) => node.type === 'rule' && !node.selectors.includes(`.pf-${version}-t-dark`) && (!node.selectors || !node.selectors.some((item) => item.includes(`.pf-${version}-theme-dark`))) // exclude dark theme blocks since dark theme variable values override default token values ) .map((node) => node.declarations.filter((decl) => decl.type === 'declaration')) .reduce((acc, val) => acc.concat(val), []); // flatten const formatFilePathToName = (filePath) => { // const filePathArr = filePath.split('/'); let prefix = ''; if (filePath.includes('components/')) { prefix = 'c_'; } else if (filePath.includes('layouts/')) { prefix = 'l_'; } return `${prefix}${basename(filePath, '.css').replace(/-+/g, '_')}`; }; const getLocalVarsMap = (cssFiles) => { const res = {}; cssFiles.forEach((filePath) => { const cssAst = parse(readFileSync(filePath, 'utf8')); getDeclarations(cssAst).forEach(({ property, value, parent }) => { if (res[property]) { // Accounts for multiple delcarations out of root scope. // TODO: revamp CSS var mapping return; } if (property.startsWith(`--pf-${version}`)) { res[property] = { ...res[property], [parent.selectors[0]]: value }; } else if (property.startsWith('--pf-t')) { res[property] = { ...res[property], [parent.selectors[0]]: value }; } }); }); return res; }; /** * Generates tokens from CSS in node_modules/@patternfly/patternfly/** * * @returns {object} of form { * c_about_modal_box: { * ".pf-c-about-modal-box" : { * "global_Color_100": { * "name": "--pf-${version}-global--Color--100", * "value": "#fff", * "values": [ * "--pf-${version}-global--Color--light-100", * "$pf-global--Color--light-100", * "$pf-color-white", * "#fff" * ] * }, * }, * ".pf-c-about-modal-box .pf-c-card": {} * } * } */ export function generateTokens() { const cssFiles = glob .sync(['{**/{components,layouts}/**/*.css', '**/patternfly-charts.css', '**/patternfly-variables.css}'].join(','), { cwd: pfStylesDir, ignore: ['assets/**', '/**/_index.css'], absolute: true }) // Sort to put variables and charts at END of list so getLocalVarsMap returns correct values .sort((a, b) => (a.split(sep).length < b.split(sep).length ? 1 : -1)); // various lookup tables to resolve variables const scssVariables = readFileSync( require.resolve('@patternfly/patternfly/sass-utilities/scss-variables.scss'), 'utf8' ); const scssVarsMap = getRegexMatches(scssVariables, /(\$.*):\s*([^;^!]+)/g); const cssGlobalVariablesAst = parse( readFileSync(require.resolve('@patternfly/patternfly/base/patternfly-variables.css'), 'utf8') ); cssGlobalVariablesAst.stylesheet.rules = cssGlobalVariablesAst.stylesheet.rules.filter( (node) => !node.selectors || !node.selectors.some((item) => item.includes(`.pf-${version}-theme-dark`)) ); const cssGlobalVariablesMap = getRegexMatches(stringify(cssGlobalVariablesAst), /(--pf-[\w-]*):\s*([\w -_]+);/g); const getComputedCSSVarValue = (value, selector, varMap) => value.replace(/var\(([\w-]*)(,.*)?\)/g, (full, m1, m2) => { if (m1.startsWith(`--pf-${version}-global`)) { if (varMap[m1]) { return varMap[m1] + (m2 || ''); } else { return full; } } else { if (selector) { return getFromLocalVarsMap(m1, selector) + (m2 || ''); } } }); const getComputedScssVarValue = (value) => value.replace(/\$pf[^,)\s*/]*/g, (match) => { if (scssVarsMap[match]) { return scssVarsMap[match]; } else { return match; } }); const getVarsMap = (value, selector) => { // evaluate the value and follow the variable chain const varsMap = [value]; let computedValue = value; let finalValue = value; while (finalValue.includes('var(--pf') || computedValue.includes('var(--pf') || computedValue.includes('$pf-')) { // keep following the variable chain until we get to a value if (finalValue.includes('var(--pf')) { finalValue = getComputedCSSVarValue(finalValue, selector, cssGlobalVariablesMap); } if (computedValue.includes('var(--pf')) { computedValue = getComputedCSSVarValue(computedValue, selector); } else { computedValue = getComputedScssVarValue(computedValue); } // error out if variable doesn't exist to avoid infinite loop if (finalValue === value && computedValue === value) { // eslint-disable-next-line no-console console.error(`Error: "${value}" variable not found`); throw Error; } varsMap.push(computedValue); } const lastElement = varsMap[varsMap.length - 1]; if (lastElement.includes('pf-')) { varsMap.push(finalValue); } // all values should not be boxed by var() return varsMap.map((variable) => variable.replace(/var\(([\w-]*)\)/g, (_, match) => match)); }; // pre-populate the localVarsMap so we can lookup local variables within or across files, e.g. if we have the declaration: // --pf-${version}-c-chip-group--MarginBottom: calc(var(--pf-${version}-c-chip-group--c-chip--MarginBottom) * -1); // then we need to find: // --pf-${version}-c-chip-group--c-chip--MarginBottom: var(--pf-${version}-global--spacer--xs); const localVarsMap = getLocalVarsMap(cssFiles); const getFromLocalVarsMap = (match, selector) => { if (localVarsMap[match]) { // have exact selectors match if (localVarsMap[match][selector]) { return localVarsMap[match][selector]; } else if (Object.keys(localVarsMap[match]).length === 1) { // only one match, return its value return Object.values(localVarsMap[match])[0]; } else { // find the nearest parent selector and return its value let bestMatch = ''; let bestValue = ''; for (const key in localVarsMap[match]) { if (localVarsMap[match].hasOwnProperty(key)) { // remove trailing * from key to compare let sanitizedKey = key.replace(/\*$/, '').trim(); sanitizedKey = sanitizedKey.replace(/>$/, '').trim(); sanitizedKey = sanitizedKey.replace(/\[.*\]$/, '').trim(); // is key a parent of selector? if (selector.indexOf(sanitizedKey) > -1) { if (sanitizedKey.length > bestMatch.length) { // longest matching key is the winner bestMatch = key; bestValue = localVarsMap[match][key]; } } } } if (!bestMatch) { // eslint-disable-next-line no-console console.error(`no matching selector found for ${match} in localVarsMap`); } return bestValue; } } else { // eslint-disable-next-line no-console console.error(`no matching property found for ${match} in localVarsMap`); } }; const fileTokens = {}; cssFiles.forEach((filePath) => { const cssAst = parse(readFileSync(filePath, 'utf8')); // key is the formatted file name, e.g. c_about_modal_box const key = formatFilePathToName(filePath); getDeclarations(cssAst) .filter(({ property }) => property.startsWith('--pf')) .forEach(({ property, value, parent }) => { const selector = parent.selectors[0]; const varsMap = getVarsMap(value, selector); const propertyObj = { name: property, value: varsMap[varsMap.length - 1] }; if (varsMap.length > 1) { propertyObj.values = varsMap; } fileTokens[key] = fileTokens[key] || {}; fileTokens[key][selector] = fileTokens[key][selector] || {}; fileTokens[key][selector][formatCustomPropertyName(property)] = propertyObj; }); }); return fileTokens; }