diff --git a/packages/patternfly-4/react-tokens/src/__snapshots__/generateTokens.test.js.snap b/packages/patternfly-4/react-tokens/src/__snapshots__/generateTokens.test.js.snap index eba402baf48..c35bd7ff58c 100644 --- a/packages/patternfly-4/react-tokens/src/__snapshots__/generateTokens.test.js.snap +++ b/packages/patternfly-4/react-tokens/src/__snapshots__/generateTokens.test.js.snap @@ -66,18 +66,18 @@ export default global_BackgroundColor_100 exports[`keeps variable reference if computing fails 1`] = ` Object { - "esm/c_button_BackgroundColor.js": "export default {\\"name\\":\\"--pf-c-button--BackgroundColor\\",\\"value\\":\\"var(--pf-global--BackgroundColor--100)\\",\\"var\\":\\"var(--pf-c-button--BackgroundColor)\\"} + "esm/c_button_BackgroundColor.js": "export default {\\"name\\":\\"--pf-c-button--BackgroundColor\\",\\"value\\":\\"var(--pf-global--BackgroundColor--100)\\",\\"var\\":\\"var(--pf-c-button--BackgroundColor)\\",\\"global\\":\\"var(--pf-global--BackgroundColor--100)\\"} ", - "esm/index.js": "export const c_button_BackgroundColor = {\\"name\\":\\"--pf-c-button--BackgroundColor\\",\\"value\\":\\"var(--pf-global--BackgroundColor--100)\\",\\"var\\":\\"var(--pf-c-button--BackgroundColor)\\"} + "esm/index.js": "export const c_button_BackgroundColor = {\\"name\\":\\"--pf-c-button--BackgroundColor\\",\\"value\\":\\"var(--pf-global--BackgroundColor--100)\\",\\"var\\":\\"var(--pf-c-button--BackgroundColor)\\",\\"global\\":\\"var(--pf-global--BackgroundColor--100)\\"} ", - "js/c_button_BackgroundColor.d.ts": "const c_button_BackgroundColor: {\\"name\\": \\"--pf-c-button--BackgroundColor\\"; \\"value\\": \\"var(--pf-global--BackgroundColor--100)\\"; \\"var\\": \\"var(--pf-c-button--BackgroundColor)\\";} + "js/c_button_BackgroundColor.d.ts": "const c_button_BackgroundColor: {\\"name\\": \\"--pf-c-button--BackgroundColor\\"; \\"value\\": \\"var(--pf-global--BackgroundColor--100)\\"; \\"var\\": \\"var(--pf-c-button--BackgroundColor)\\"; \\"global\\": \\"var(--pf-global--BackgroundColor--100)\\";} export default c_button_BackgroundColor ", - "js/c_button_BackgroundColor.js": "module.exports = {\\"name\\":\\"--pf-c-button--BackgroundColor\\",\\"value\\":\\"var(--pf-global--BackgroundColor--100)\\",\\"var\\":\\"var(--pf-c-button--BackgroundColor)\\"} + "js/c_button_BackgroundColor.js": "module.exports = {\\"name\\":\\"--pf-c-button--BackgroundColor\\",\\"value\\":\\"var(--pf-global--BackgroundColor--100)\\",\\"var\\":\\"var(--pf-c-button--BackgroundColor)\\",\\"global\\":\\"var(--pf-global--BackgroundColor--100)\\"} ", - "js/index.d.ts": "export const c_button_BackgroundColor: { \\"name\\": \\"--pf-c-button--BackgroundColor\\"; \\"value\\": \\"var(--pf-global--BackgroundColor--100)\\"; \\"var\\": \\"var(--pf-c-button--BackgroundColor)\\"; } + "js/index.d.ts": "export const c_button_BackgroundColor: { \\"name\\": \\"--pf-c-button--BackgroundColor\\"; \\"value\\": \\"var(--pf-global--BackgroundColor--100)\\"; \\"var\\": \\"var(--pf-c-button--BackgroundColor)\\"; \\"global\\": \\"var(--pf-global--BackgroundColor--100)\\"; } ", - "js/index.js": "module.exports.c_button_BackgroundColor = {\\"name\\":\\"--pf-c-button--BackgroundColor\\",\\"value\\":\\"var(--pf-global--BackgroundColor--100)\\",\\"var\\":\\"var(--pf-c-button--BackgroundColor)\\"} + "js/index.js": "module.exports.c_button_BackgroundColor = {\\"name\\":\\"--pf-c-button--BackgroundColor\\",\\"value\\":\\"var(--pf-global--BackgroundColor--100)\\",\\"var\\":\\"var(--pf-c-button--BackgroundColor)\\",\\"global\\":\\"var(--pf-global--BackgroundColor--100)\\"} ", } `; diff --git a/packages/patternfly-4/react-tokens/src/generateTokens.js b/packages/patternfly-4/react-tokens/src/generateTokens.js index 9183a616a25..9ede7b60af2 100644 --- a/packages/patternfly-4/react-tokens/src/generateTokens.js +++ b/packages/patternfly-4/react-tokens/src/generateTokens.js @@ -8,17 +8,99 @@ const outDir = resolve(__dirname, '../dist'); const pfStylesDir = dirname(require.resolve('@patternfly/patternfly/patternfly.css')); const templateDir = resolve(__dirname, './templates'); -const cssFiles = glob.sync('**/*.css', { +// const cssFiles = glob.sync('**/*.css', { +// cwd: pfStylesDir, +// ignore: ['assets/**'] +// }); +const cssFiles = glob.sync('**/{components,layouts}/**/*.css', { cwd: pfStylesDir, ignore: ['assets/**'] }); +// need 2 lookup tables +// first lookup table (cssVarsMap): global variable -> variable, e.g. (Global variable): --pf-global--BackgroundColor--100 --> (Variable): $pf-global--BackgroundColor--100 +// second lookup table (scssVarsMap): variable -> color, e.g. (Variable): $pf-global--BackgroundColor--100 --> (Palette color): $pf-color-white + +const varsRegex = /(--.*):\s*#{(\$pf-[^\s]+)}/g; +const variables = readFileSync(require.resolve('@patternfly/patternfly/_variables.scss'), 'utf8'); +let matches; +let cssVarsMap = {}; +while ((matches = varsRegex.exec(variables))) { + cssVarsMap[matches[1]] = matches[2]; +} + +// this regex captures the variables name and the variable value, example: +// given: $pf-global--BackgroundColor--100: $pf-color-white !default; +// extract: group 1: $pf-global--BackgroundColor--100, group 2: $pf-color-white +// const scssVarsRegex = /(\$.*):\s*(\$pf-color[^\s]+)/g; +const scssVarsRegex = /(\$.*):\s*([^;^!]+)/g; +// contains default values and mappings to colors.scss for color values +const scssVariables = readFileSync( + require.resolve('@patternfly/patternfly/sass-utilities/scss-variables.scss'), + 'utf8' +); +matches = null; +let scssVarsMap = {}; +while ((matches = scssVarsRegex.exec(scssVariables))) { + scssVarsMap[matches[1]] = matches[2].trim(); +} + +const colorsRegex = /(\$.*):\s*([^;^!]+)/g; +// contains default values and mappings to colors.scss for color values +const colorVariables = readFileSync(require.resolve('@patternfly/patternfly/sass-utilities/colors.scss'), 'utf8'); +matches = null; +let colorsMap = {}; +while ((matches = colorsRegex.exec(colorVariables))) { + colorsMap[matches[1]] = matches[2].trim(); +} + +const allVariablesRegex = /(--.*):\s*(.*);/g; +// contains default values and mappings to colors.scss for color values +const allVariables = readFileSync(require.resolve('@patternfly/patternfly/patternfly-variables.css'), 'utf8'); +matches = null; +let allVariablesMap = {}; +while ((matches = allVariablesRegex.exec(allVariables))) { + allVariablesMap[matches[1]] = matches[2].trim(); +} + const formatCustomPropertyName = key => key.replace('--pf-', '').replace(/-+/g, '_'); +const getGlobalVar = (tokenObj, value, property) => { + // check if the value starts with --pf-global + const innerValueMatch = /var\(([\w|-]*)\)/g.exec(value); + let globalVar; + if (innerValueMatch && innerValueMatch[1] && innerValueMatch[1].startsWith('--pf-global')) { + // component token that has a global variable value + globalVar = innerValueMatch[1]; // e.g. --pf-global--BackgroundColor--100 + const scssVariable = scssVarsMap[cssVarsMap[globalVar]]; // e.g. $pf-color-white + tokenObj.globalVar = globalVar; + if (cssVarsMap[globalVar]) { + tokenObj.scssKey = cssVarsMap[globalVar]; + tokenObj.scssValue = scssVariable; + } else { + console.log(`${key}: ${cssVariable}`); + } + } else if (property && property.startsWith('--pf-global')) { + // global variable token + globalVar = property; + const scssVariable = scssVarsMap[cssVarsMap[globalVar]]; // e.g. $pf-color-white + tokenObj.globalVar = globalVar; + if (cssVarsMap[globalVar]) { + tokenObj.scssKey = cssVarsMap[globalVar]; + tokenObj.scssValue = scssVariable; + } else { + console.log(`${key}: ${cssVariable}`); + } + } + return globalVar ? allVariablesMap[globalVar] : value; +}; + const tokens = {}; cssFiles.forEach(filePath => { const absFilePath = resolve(pfStylesDir, filePath); const cssAst = parse(readFileSync(absFilePath, 'utf8')); + // keeps a mapping of the locally used variables + const localVarsMap = {}; cssAst.stylesheet.rules.forEach(node => { if (node.type !== 'rule' || node.selectors.indexOf('.pf-t-dark') !== -1) { return; @@ -28,25 +110,87 @@ cssFiles.forEach(filePath => { if (decl.type !== 'declaration') { return; } - const { property, value } = decl; - if (decl.property.startsWith('--')) { + const { property, value, parent } = decl; + if (decl.property.startsWith('--') && !decl.property.startsWith('--pf-global')) { const key = formatCustomPropertyName(property); - const populatedValue = value.replace(/var\(([\w|-]*)\)/g, (full, match) => { - const computedValue = tokens[formatCustomPropertyName(match)]; - return computedValue ? computedValue.value : `var(${match})`; + localVarsMap[property] = value; + let computedValueObjects = []; + const computedValue = value.replace(/var\(([\w|-]*)\)/g, (full, match) => { + let valueObj = {}; + let populatedValue; + if (match.startsWith('--pf-global')) { + populatedValue = getGlobalVar(valueObj, `var(${match})`); + } else { + populatedValue = localVarsMap[match]; + if (populatedValue) { + if (populatedValue.startsWith('var(--pf-global')) { + populatedValue = getGlobalVar(valueObj, populatedValue); + } + valueObj.localVar = match; + } + } + const checkedForNumValue = !isNaN(populatedValue) ? Number(populatedValue).valueOf() : populatedValue; + computedValueObjects.push(valueObj); + return populatedValue || full; }); // Avoid stringifying numeric chart values - const chartNum = decl.property.startsWith('--pf-chart-') && !isNaN(populatedValue); - tokens[key] = { - name: property, - value: chartNum ? Number(populatedValue).valueOf() : populatedValue, - var: `var(${property})` - }; + const chartNum = decl.property.startsWith('--pf-chart-') && !isNaN(computedValue); + const checkedValue = chartNum ? Number(computedValue).valueOf() : computedValue; + + // if the token already exists, it means that we are pushing the variable again but in a different context (different CSS selectors) + // we need to keep a list of the values + + let newValue; + if (tokens[key]) { + newValue = { + selector: parent.selectors[0], + value: checkedValue + }; + if (computedValueObjects.length === 1) { + newValue = { + ...newValue, + ...computedValueObjects[0] + } + } else { + newValue.computedValueObjects = computedValueObjects; + } + tokens[key].values.push(newValue); + } else { + newValue = { + selector: parent.selectors[0], + value: checkedValue + }; + if (computedValueObjects.length === 1) { + newValue = { + ...newValue, + ...computedValueObjects[0] + } + } else { + newValue.computedValueObjects = computedValueObjects; + } + tokens[key] = { + name: property, + value: checkedValue, + var: `var(${property})`, + values: [newValue] + }; + } } }); }); }); +Object.entries(allVariablesMap).forEach(globalVar => { + const key = formatCustomPropertyName(globalVar[0]); + const variable = globalVar[0]; + const value = globalVar[1]; + tokens[key] = { + name: variable, + var: `var(${variable})`, + value + }; +}); + readdirSync(templateDir).forEach(templateFile => { const template = require(join(templateDir, templateFile)); outputFileSync(template.getOutputPath({ outDir }), template.getContent({ tokens })); diff --git a/packages/patternfly-4/react-tokens/src/generateTokensBySelector.js b/packages/patternfly-4/react-tokens/src/generateTokensBySelector.js new file mode 100644 index 00000000000..3565dd9c649 --- /dev/null +++ b/packages/patternfly-4/react-tokens/src/generateTokensBySelector.js @@ -0,0 +1,196 @@ +const glob = require('glob'); +const { dirname, resolve, join } = require('path'); +const { parse } = require('css'); +const { readFileSync, readdirSync } = require('fs'); +const { outputFileSync } = require('fs-extra'); + +const outDir = resolve(__dirname, '../dist'); +const pfStylesDir = dirname(require.resolve('@patternfly/patternfly/patternfly.css')); +const templateDir = resolve(__dirname, './templates'); + +const cssFiles = glob.sync('**/{components,layouts}/**/*.css', { + cwd: pfStylesDir, + ignore: ['assets/**'] +}); + +const formatCustomPropertyName = filePath => { + const filePathArr = filePath.split('/'); + return `${filePathArr[0] === 'components' ? 'c_' : 'l_'}${filePathArr[1]}`; +}; + +// need 2 lookup tables +// first lookup table (cssVarsMap): global variable -> variable, e.g. (Global variable): --pf-global--BackgroundColor--100 --> (Variable): $pf-global--BackgroundColor--100 +// second lookup table (scssVarsMap): variable -> color, e.g. (Variable): $pf-global--BackgroundColor--100 --> (Palette color): $pf-color-white + +const varsRegex = /(--.*):\s*#{(\$pf-[^\s]+)}/g; +const variables = readFileSync(require.resolve('@patternfly/patternfly/_variables.scss'), 'utf8'); +let matches; +let cssVarsMap = {}; +while ((matches = varsRegex.exec(variables))) { + cssVarsMap[matches[1]] = matches[2]; +} + +// this regex captures the variables name and the variable value, example: +// given: $pf-global--BackgroundColor--100: $pf-color-white !default; +// extract: group 1: $pf-global--BackgroundColor--100, group 2: $pf-color-white +// const scssVarsRegex = /(\$.*):\s*(\$pf-color[^\s]+)/g; +const scssVarsRegex = /(\$.*):\s*([^;^!]+)/g; +// contains default values and mappings to colors.scss for color values +const scssVariables = readFileSync( + require.resolve('@patternfly/patternfly/sass-utilities/scss-variables.scss'), + 'utf8' +); +matches = null; +let scssVarsMap = {}; +while ((matches = scssVarsRegex.exec(scssVariables))) { + scssVarsMap[matches[1]] = matches[2].trim(); +} + +const colorsRegex = /(\$.*):\s*([^;^!]+)/g; +// contains default values and mappings to colors.scss for color values +const colorVariables = readFileSync(require.resolve('@patternfly/patternfly/sass-utilities/colors.scss'), 'utf8'); +matches = null; +let colorsMap = {}; +while ((matches = colorsRegex.exec(colorVariables))) { + colorsMap[matches[1]] = matches[2].trim(); +} + +const allVariablesRegex = /(--.*):\s*(.*);/g; +// contains default values and mappings to colors.scss for color values +const allVariables = readFileSync(require.resolve('@patternfly/patternfly/patternfly-variables.css'), 'utf8'); +matches = null; +let allVariablesMap = {}; +while ((matches = allVariablesRegex.exec(allVariables))) { + allVariablesMap[matches[1]] = matches[2].trim(); +} + +const getGlobalVar = (tokenObj, value, property) => { + // check if the value starts with --pf-global + const innerValueMatch = /var\(([\w|-]*)\)/g.exec(value); + let globalVar; + if (innerValueMatch && innerValueMatch[1] && innerValueMatch[1].startsWith('--pf-global')) { + // component token that has a global variable value + globalVar = innerValueMatch[1]; // e.g. --pf-global--BackgroundColor--100 + const scssVariable = scssVarsMap[cssVarsMap[globalVar]]; // e.g. $pf-color-white + tokenObj.globalVar = globalVar; + if (cssVarsMap[globalVar]) { + tokenObj.scssKey = cssVarsMap[globalVar]; + tokenObj.scssValue = scssVariable; + } else { + console.log(`${key}: ${cssVariable}`); + } + } else if (property && property.startsWith('--pf-global')) { + // global variable token + globalVar = property; + const scssVariable = scssVarsMap[cssVarsMap[globalVar]]; // e.g. $pf-color-white + tokenObj.globalVar = globalVar; + if (cssVarsMap[globalVar]) { + tokenObj.scssKey = cssVarsMap[globalVar]; + tokenObj.scssValue = scssVariable; + } else { + console.log(`${key}: ${cssVariable}`); + } + } + tokenObj.computedValue = globalVar ? allVariablesMap[globalVar] : value; +}; + +const tokens = {}; +cssFiles.forEach(filePath => { + const absFilePath = resolve(pfStylesDir, filePath); + const cssAst = parse(readFileSync(absFilePath, 'utf8')); + console.log(filePath); + const localVarsMap = {}; + cssAst.stylesheet.rules.forEach(node => { + if (node.type !== 'rule' || node.selectors.indexOf('.pf-t-dark') !== -1) { + return; + } + + node.declarations.forEach(decl => { + if (decl.type !== 'declaration') { + return; + } + const { property, value, parent } = decl; + if (decl.property.startsWith('--') && !decl.property.startsWith('--pf-global')) { + // parse variables + // each token we create will have the form of: + /* + [{ + selector: '.pf-c-about-modal-box', + properties: [{ + name: '--pf-global--Color--100', + value: 'var(--pf-global--Color--light-100)' + }] + }, { + selector: '.pf-c-about-modal-box .button', + properties: [{ + name: '--pf-c-button--m-primary--Color', + value: 'var(--pf-global--primary-color--dark-100)' + }, { + name: '--pf-c-button--m-primary--hover--Color', + value: 'var(--pf-global--primary-color--dark-100)' + }] + }] + */ + + // key is the root of the selector, e.g. c_about_modal_box + const key = formatCustomPropertyName(filePath); + localVarsMap[property] = value; + let newSelector; + let computedValueObjects = []; + const computedValue = value.replace(/var\(([\w|-]*)\)/g, (full, match) => { + let valueObj = {}; + if (match.startsWith('--pf-global')) { + getGlobalVar(valueObj, `var(${match})`); + } else { + const computedValue = localVarsMap[match]; + if (computedValue) { + if (computedValue.startsWith('var(--pf-global')) { + getGlobalVar(valueObj, computedValue); + } else { + valueObj.computedValue = computedValue; + } + valueObj.localVar = match; + } + } + computedValueObjects.push(valueObj); + return (valueObj && valueObj.computedValue) || full; + }); + if (tokens[key]) { + // check if selector exists + const index = tokens[key].findIndex(item => item.selector === parent.selectors[0]); + if (index >= 0) { + let newProperty = { + name: property, + value, + computedValue, + computedValueObjects + }; + tokens[key][index].properties.push(newProperty); + } else { + newSelector = { + selector: parent.selectors[0], + properties: [{ + name: property, + value, + computedValue, + computedValueObjects + }] + }; + tokens[key].push(newSelector); + } + } else { + newSelector = { + selector: parent.selectors[0], + properties: [{ + name: property, + value, + computedValue, + computedValueObjects + }] + }; + tokens[key] = [newSelector] + } + } + }); + }); +}); diff --git a/packages/patternfly-4/react-tokens/src/templates/cjs.js b/packages/patternfly-4/react-tokens/src/templates/cjs.js index 683e4f2fff6..2a1c75162d0 100644 --- a/packages/patternfly-4/react-tokens/src/templates/cjs.js +++ b/packages/patternfly-4/react-tokens/src/templates/cjs.js @@ -3,7 +3,7 @@ const { join } = require('path'); module.exports = { getOutputPath: ({ outDir }) => join(outDir, 'js/index.js'), getContent: ({ tokens }) => - Object.keys(tokens).reduce((acc, key) => `${acc}module.exports.${key} = ${JSON.stringify(tokens[key])}\n`, ''), + Object.keys(tokens).reduce((acc, key) => `${acc}module.exports.${key} = ${JSON.stringify(tokens[key], null, 2)}\n`, ''), getSingleOutputPath: ({ outDir, tokenName }) => join(outDir, `js/${tokenName}.js`), - getSingleContent: ({ tokenValue }) => `module.exports = ${JSON.stringify(tokenValue)}\n` + getSingleContent: ({ tokenValue }) => `module.exports = ${JSON.stringify(tokenValue, null, 2)}\n` }; diff --git a/packages/patternfly-4/react-tokens/src/templates/d.ts.js b/packages/patternfly-4/react-tokens/src/templates/d.ts.js index 75f3d640dcc..a22394eb0dd 100644 --- a/packages/patternfly-4/react-tokens/src/templates/d.ts.js +++ b/packages/patternfly-4/react-tokens/src/templates/d.ts.js @@ -6,13 +6,13 @@ module.exports = { Object.keys(tokens).reduce((acc, key) => { const token = tokens[key]; const tokenTypeValue = Object.keys(token) - .map(tokenKey => `${JSON.stringify(tokenKey)}: ${JSON.stringify(token[tokenKey])};`) + .map(tokenKey => `${JSON.stringify(tokenKey)}: ${JSON.stringify(token[tokenKey], null, 2)};`) .join(' '); return `${acc}export const ${key}: { ${tokenTypeValue} }\n`; }, ''), getSingleOutputPath: ({ outDir, tokenName }) => join(outDir, `js/${tokenName}.d.ts`), getSingleContent: ({ tokenName, tokenValue }) => `const ${tokenName}: {${Object.entries(tokenValue) - .map(([key, value]) => `${JSON.stringify(key)}: ${JSON.stringify(value)};`) + .map(([key, value]) => `${JSON.stringify(key)}: ${JSON.stringify(value, null, 2)};`) .join(' ')}}\nexport default ${tokenName}\n` }; diff --git a/packages/patternfly-4/react-tokens/src/templates/esm.js b/packages/patternfly-4/react-tokens/src/templates/esm.js index dff736e4e34..9c565ebe0ae 100644 --- a/packages/patternfly-4/react-tokens/src/templates/esm.js +++ b/packages/patternfly-4/react-tokens/src/templates/esm.js @@ -3,7 +3,7 @@ const { join } = require('path'); module.exports = { getOutputPath: ({ outDir }) => join(outDir, 'esm/index.js'), getContent: ({ tokens }) => - Object.keys(tokens).reduce((acc, key) => `${acc}export const ${key} = ${JSON.stringify(tokens[key])}\n`, ''), + Object.keys(tokens).reduce((acc, key) => `${acc}export const ${key} = ${JSON.stringify(tokens[key], null, 2)}\n`, ''), getSingleOutputPath: ({ outDir, tokenName }) => join(outDir, `esm/${tokenName}.js`), - getSingleContent: ({ tokenValue }) => `export default ${JSON.stringify(tokenValue)}\n` + getSingleContent: ({ tokenValue }) => `export default ${JSON.stringify(tokenValue, null, 2)}\n` };