diff --git a/__tests__/formats/__snapshots__/all.test.js.snap b/__tests__/formats/__snapshots__/all.test.js.snap index 86f5b33de..1f63a876e 100644 --- a/__tests__/formats/__snapshots__/all.test.js.snap +++ b/__tests__/formats/__snapshots__/all.test.js.snap @@ -83,6 +83,18 @@ exports[`formats all should match css/variables snapshot 1`] = ` " `; +exports[`formats all should match css/variables-deep snapshot 1`] = ` +"/** + * Do not edit directly + * Generated on Sat, 01 Jan 2000 00:00:00 GMT + */ + +:root { + --color_red: #FF0000; /* comment */ +} +" +`; + exports[`formats all should match flutter/class.dart snapshot 1`] = ` " // diff --git a/__tests__/formats/__snapshots__/cssVariablesDeep.test.js.snap b/__tests__/formats/__snapshots__/cssVariablesDeep.test.js.snap new file mode 100644 index 000000000..1f536100c --- /dev/null +++ b/__tests__/formats/__snapshots__/cssVariablesDeep.test.js.snap @@ -0,0 +1,31 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`formats css/variables-deep should generate a css variables file with alias references 1`] = ` +"/** + * Do not edit directly + * Generated on Sat, 01 Jan 2000 00:00:00 GMT + */ + +:root { + --color-blue-20: #0000ff; + --primary: var(--color-blue-20); + --icon: \\"acme-icon\\"; + --width: 100px; +} +" +`; + +exports[`formats css/variables-deep should work with a prefix 1`] = ` +"/** + * Do not edit directly + * Generated on Sat, 01 Jan 2000 00:00:00 GMT + */ + +:root { + --color-blue-20: #0000ff; + --primary: var(--acme-color-blue-20); + --icon: \\"acme-icon\\"; + --width: 100px; +} +" +`; diff --git a/__tests__/formats/cssVariablesDeep.test.js b/__tests__/formats/cssVariablesDeep.test.js new file mode 100644 index 000000000..cbcc30758 --- /dev/null +++ b/__tests__/formats/cssVariablesDeep.test.js @@ -0,0 +1,71 @@ +var fs = require('fs-extra'); +var helpers = require('../__helpers'); +const formats = require('../../lib/common/formats'); + +var file = { + "destination": "__output/", + "format": "css/variables-deep" +}; + +var dictionary = { + "allProperties": [ + { + name: "color-blue-20", + value: "#0000ff", + original: { + value: "#0000ff", + }, + }, + { + name: "primary", + value: "#00ff00", + original: { + value: "{color.blue-20.value}", + }, + }, + { + name: "icon", + value: "acme-icon", + attributes: { "data-type": "string" }, + }, + { + name: "width", + value: "100px", + attributes: { "data-type": "number" }, + } + ] +}; + +describe('formats', () => { + describe('css/variables-deep', () => { + + // mock the Date.now() call to a fixed value + const constantDate = new Date('2000-01-01'); + const globalDate = global.Date; + + var formatter = formats['css/variables-deep'].bind(file); + + beforeEach(() => { + global.Date = function() { return constantDate }; + helpers.clearOutput(); + }); + + afterEach(() => { + global.Date = globalDate; + helpers.clearOutput(); + }); + + it('should generate a css variables file with alias references', () => { + fs.writeFileSync('./__tests__/__output/output.css', formatter(dictionary, {}) ); + const testFile = fs.readFileSync("./__tests__/__output/output.css", "UTF-8"); + expect(testFile).toMatchSnapshot(); + }); + + it('should work with a prefix', () => { + fs.writeFileSync('./__tests__/__output/output.css', formatter(dictionary, { prefix: "acme" }) ); + const testFile = fs.readFileSync("./__tests__/__output/output.css", "UTF-8"); + expect(testFile).toMatchSnapshot(); + }); + }); + +}); diff --git a/lib/common/formats.js b/lib/common/formats.js index 52990e688..bad8d6d41 100644 --- a/lib/common/formats.js +++ b/lib/common/formats.js @@ -13,7 +13,8 @@ var fs = require('fs'), _ = require('lodash'), - GroupMessages = require('../utils/groupMessages'); + GroupMessages = require('../utils/groupMessages'), + cssFormatter = require('./formats/css-variables'); var SASS_MAP_FORMAT_DEPRECATION_WARNINGS = GroupMessages.GROUP.SassMapFormatDeprecationWarnings; @@ -108,6 +109,27 @@ module.exports = { '\n}\n'; }, + /** + * Creates a CSS file with variable definitions that maintain their alias references + * + * @memberof Formats + * @kind member + * @example + * ```css + * :root { + * --color-primary: #f0f0f0; + * --button-primary-background-color: var(--color-primary); + * } + * ``` + */ + 'css/variables-deep': function(dictionary, config) { + const newConfig = Object.assign({}, config, { + deep: true, + header: fileHeader(this.options) + }); + return cssFormatter(dictionary, newConfig); + }, + /** * Creates a SCSS file with a flat map based on the style dictionary * diff --git a/lib/common/formats/css-variables.js b/lib/common/formats/css-variables.js new file mode 100644 index 000000000..542668058 --- /dev/null +++ b/lib/common/formats/css-variables.js @@ -0,0 +1,72 @@ +function formatter(dictionary, config) { + const header = config.header || ''; + const body = dictionary.allProperties.map(mapProp.bind(this, config)); + return `${header}:root {\n${body.join("\n")}\n}\n`; +} + +function mapProp(config, prop) { + const comment = (prop.comment) ? ` /* ${prop.comment} */` : ''; + const varName = makePropCSSVar(prop.name); + const value = setValue(prop, config); + let token = ` ${varName}: ${value};${comment}`; + + if (config.dark && prop.attributes && prop.attributes.dark) { + const darkProp = generateDarkToken(prop); + const darkValue = setValue(darkProp, config); + token += `;\n ${varName}-dark: ${darkValue};`; + } + + return token; +} + +function setValue(prop, config) { + if (typeof prop.value === "object") { + throwObjectError(prop); + } + + const nVal = prop.value; + const oVal = prop.original && prop.original.value; + if (config.deep && nVal !== oVal && isAlias(oVal)) { + return useReferenceValue(oVal, config.prefix); + } + return typeIsString(prop) ? `"${nVal}"` : nVal; +} + +function generateDarkToken(prop) { + let returnProp = prop; + if (prop.attributes && prop.attributes.dark) { + returnProp = { + name: prop.name+'-dark', + value: prop.attributes.dark.value, + original: { value: prop.attributes.dark.original }, + }; + } + return returnProp; +} + +function throwObjectError(prop) { + let message = `"${prop.name}" has an original value of "${prop.original.value}". \n`; + message += "This points to an object. "; + message += "Reference the object's \"value\" key if it's an alias \n"; + throw new Error(message); +} + +function isAlias(str) { + // is string and matches "{text.value}" + return !!(typeof str === "string" && str.match(/(^{.*\.value}$)/gm)); +} + +function typeIsString(prop) { + return !!(prop.attributes && prop.attributes["data-type"] === "string"); +} + +function makePropCSSVar(name) { + return `--${name}`; +} + +function useReferenceValue(value, prefix) { + const head = prefix ? `var(--${prefix}-` : "var(--"; + return `${head}${value.replace(/\./g, "-").replace(/({|-value})/g, "")})`; +} + +module.exports = formatter;