diff --git a/README.md b/README.md index e41ab2abe..1dd096ae5 100644 --- a/README.md +++ b/README.md @@ -227,7 +227,7 @@ While not exactly necessary, we feel this classification structure of style prop Structuring style properties in this manner gives us consistent naming and accessing of these properties. You don't need to remember if it is button_color_error or error_button_color, it is color_background_button_error! - Technically, you can organize and name your style properties however you want, there are no restrictions. But we have a good amount of helpers if you do use this structure, like the 'attribute/cti' transform which adds attributes to the property of its CTI based on the path in the object. There are a lot of name transforms as well for when you want a flat structure like for sass variables. + Technically, you can organize and name your style properties however you want, there are no restrictions. But we have a good amount of helpers if you do use this structure, like the 'attribute/cti' transform which adds attributes to the property of its CTI based on the path in the object. There are a lot of name transforms as well for when you want a flat structure like for Sass variables. Also, the CTI structure provides a good mechanism to target transforms for specific kinds of properties. All of the transforms provided by the framework use the CTI of a property to know if it should be applied. For instance, the 'color/hex' transform only applies to properties of the category 'color'. diff --git a/__tests__/extend.test.js b/__tests__/extend.test.js index 8c5765f65..7434108ef 100644 --- a/__tests__/extend.test.js +++ b/__tests__/extend.test.js @@ -61,10 +61,33 @@ describe('extend', () => { ).toThrow('include must be an array'); }); - it('should throw if a path in the includes array doesnt resolve', () => { - expect( - StyleDictionary.extend.bind(null, {include: ['foo']}) - ).toThrow("Cannot find module 'foo'"); + it('should not update properties if include glob paths dont resolve to anything', () => { + var StyleDictionaryExtended = StyleDictionary.extend({ + include: ['foo'] + }); + expect(typeof StyleDictionaryExtended.properties.size).toBe('undefined'); + }); + + it('should properly glob paths', () => { + var StyleDictionaryExtended = StyleDictionary.extend({ + include: [__dirname + '/__properties/*.json'] + }); + expect(typeof StyleDictionaryExtended.properties.size.padding.tiny).toBe('object'); + }); + + it('should build the properties object if an include is given', () => { + var StyleDictionaryExtended = StyleDictionary.extend({ + "include": [__dirname + "/__properties/paddings.json"] + }); + expect(StyleDictionaryExtended.properties).toEqual(helpers.fileToJSON(__dirname + "/__properties/paddings.json")); + }); + + it('should override existing properties if include is given', () => { + var StyleDictionaryExtended = StyleDictionary.extend({ + properties: test_props, + include: [__dirname + "/__properties/paddings.json"] + }); + expect(StyleDictionaryExtended.properties).toEqual(helpers.fileToJSON(__dirname + "/__properties/paddings.json")); }); it('should update properties if there are includes', () => { @@ -91,10 +114,11 @@ describe('extend', () => { ).toThrow('source must be an array'); }); - it('should throw if a path in the source array doesnt resolve', () => { - expect( - StyleDictionary.extend.bind(null, {include: ['foo']}) - ).toThrow("Cannot find module 'foo'"); + it('should not update properties if source glob paths don\'t resolve to anything', () => { + var StyleDictionaryExtended = StyleDictionary.extend({ + source: ['foo'] + }); + expect(typeof StyleDictionaryExtended.properties.size).toBe('undefined'); }); it('should build the properties object if a source is given', () => { diff --git a/__tests__/filterProperties.test.js b/__tests__/filterProperties.test.js index 35657833f..267b1e5d1 100644 --- a/__tests__/filterProperties.test.js +++ b/__tests__/filterProperties.test.js @@ -112,16 +112,21 @@ describe('filterProperties', () => { expect(filteredDictionary.properties).not.toHaveProperty('color'); }); - it('should work with a filter object', () => { - var filter = { "attributes": { "category": "size" } }; - var filteredDictionary = filterProperties(dictionary, filter); - _.each(filteredDictionary.allProperties, function(property) { - expect(property).not.toBe(colorRed); - expect(property).not.toBe(colorBlue); + describe('should throw if', () => { + it('filter is a string', () => { + expect( + function(){ + filterProperties(dictionary, 'my_filter') + } + ).toThrow(/filter is not a function/); + }); + it('filter is an object', () => { + expect( + function(){ + filterProperties(dictionary, { "attributes": { "category": "size" } }) + } + ).toThrow(/filter is not a function/); }); - expect(filteredDictionary.allProperties).toEqual([sizeSmall, sizeLarge]); - expect(filteredDictionary.properties).toHaveProperty('size'); - expect(filteredDictionary.properties).not.toHaveProperty('color'); - }); + }); diff --git a/__tests__/formats/__snapshots__/all.test.js.snap b/__tests__/formats/__snapshots__/all.test.js.snap index de6b8e471..35be41247 100644 --- a/__tests__/formats/__snapshots__/all.test.js.snap +++ b/__tests__/formats/__snapshots__/all.test.js.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`formats all should return android/colors as a string 1`] = ` +exports[`formats all should match android/colors snapshot 1`] = ` " +<% + // for backward compatibility we need to have the user explicitly hide it + var showFileHeader = (this.options && this.options.hasOwnProperty('showFileHeader')) ? this.options.showFileHeader : true; + if(showFileHeader) { + print(""); + } +%> <% _.each(props, function(prop) { %><%= prop.value %><% if (prop.comment) { %><% } %> diff --git a/lib/common/templates/android/dimens.template b/lib/common/templates/android/dimens.template index d101497fe..792454fa2 100644 --- a/lib/common/templates/android/dimens.template +++ b/lib/common/templates/android/dimens.template @@ -17,10 +17,16 @@ var props = _.filter(allProperties, function(prop) { return prop.attributes.category === 'size' && prop.attributes.type !== 'font' }); %> - +<% + // for backward compatibility we need to have the user explicitly hide it + var showFileHeader = (this.options && this.options.hasOwnProperty('showFileHeader')) ? this.options.showFileHeader : true; + if(showFileHeader) { + print(""); + } +%> <% _.each(props, function(prop) { %><%= prop.value %><% if (prop.comment) { %><% } %> diff --git a/lib/common/templates/android/fontDimens.template b/lib/common/templates/android/fontDimens.template index 18fd9f5ee..31c3b93e4 100644 --- a/lib/common/templates/android/fontDimens.template +++ b/lib/common/templates/android/fontDimens.template @@ -17,10 +17,16 @@ var props = _.filter(allProperties, function(prop) { return prop.attributes.category === 'size' && prop.attributes.type === 'font'; }); %> - +<% + // for backward compatibility we need to have the user explicitly hide it + var showFileHeader = (this.options && this.options.hasOwnProperty('showFileHeader')) ? this.options.showFileHeader : true; + if(showFileHeader) { + print(""); + } +%> <% _.each(props, function(prop) { %><%= prop.value %><% if (prop.comment) { %><% } %> diff --git a/lib/common/templates/android/integers.template b/lib/common/templates/android/integers.template index 2f6fe980a..6404fa979 100644 --- a/lib/common/templates/android/integers.template +++ b/lib/common/templates/android/integers.template @@ -17,10 +17,16 @@ var props = _.filter(allProperties, function(prop) { return prop.attributes.category === 'time'; }); %> - +<% + // for backward compatibility we need to have the user explicitly hide it + var showFileHeader = (this.options && this.options.hasOwnProperty('showFileHeader')) ? this.options.showFileHeader : true; + if(showFileHeader) { + print(""); + } +%> <% _.each(props, function(prop) { %><%= prop.value %><% if (prop.comment) { %><% } %> diff --git a/lib/common/templates/android/strings.template b/lib/common/templates/android/strings.template index 75afcba63..51ae200e5 100644 --- a/lib/common/templates/android/strings.template +++ b/lib/common/templates/android/strings.template @@ -17,10 +17,16 @@ var props = _.filter(allProperties, function(prop) { return prop.attributes.category === 'content'; }); %> - +<% + // for backward compatibility we need to have the user explicitly hide it + var showFileHeader = (this.options && this.options.hasOwnProperty('showFileHeader')) ? this.options.showFileHeader : true; + if(showFileHeader) { + print(""); + } +%> <% _.each(props, function(prop) { %><%= prop.value %><% if (prop.comment) { %><% } %> diff --git a/lib/common/templates/sass/map-deep.template b/lib/common/templates/scss/map-deep.template similarity index 100% rename from lib/common/templates/sass/map-deep.template rename to lib/common/templates/scss/map-deep.template diff --git a/lib/common/templates/sass/map-flat.template b/lib/common/templates/scss/map-flat.template similarity index 100% rename from lib/common/templates/sass/map-flat.template rename to lib/common/templates/scss/map-flat.template diff --git a/lib/extend.js b/lib/extend.js index aa271e8a1..7b94bb91d 100644 --- a/lib/extend.js +++ b/lib/extend.js @@ -103,9 +103,7 @@ function extend(opts) { if (!_.isArray(options.include)) throw new Error('include must be an array'); - _.forEach(options.include, function(file){ - to_ret.properties = deepExtend([{}, to_ret.properties, require(file)]); - }); + to_ret.properties = combineJSON(options.include, true); to_ret.include = null; // We don't want to carry over include references } diff --git a/lib/filterProperties.js b/lib/filterProperties.js index 904711cf8..899bfc4af 100644 --- a/lib/filterProperties.js +++ b/lib/filterProperties.js @@ -70,10 +70,9 @@ function filterPropertyArray(properties, filter) { * object using a function provided by the user. * * @param {Object} dictionary - * @param {Function|Object} filter - Either a function that receives a property - * object and returns `true` if the property should be included in the output - * or `false` if the property should be excluded from the output, or an object - * to compare each property to (see lodash/matches). + * @param {Function} filter - A function that receives a property object + * and returns `true` if the property should be included in the output + * or `false` if the property should be excluded from the output * @returns {Object} dictionary - A new dictionary containing only the * properties that matched the filter (or the original dictionary if no filter * function was provided). @@ -82,10 +81,9 @@ function filterProperties(dictionary, filter) { if (!filter) { return dictionary } else { - const filterFunc = _.isFunction(filter) ? filter : _.matches(filter) return _.assign({}, dictionary, { - allProperties: filterPropertyArray(dictionary.allProperties, filterFunc), - properties: filterPropertyObject(dictionary.properties, filterFunc) + allProperties: filterPropertyArray(dictionary.allProperties, filter), + properties: filterPropertyObject(dictionary.properties, filter) }) } } diff --git a/lib/register/filter.js b/lib/register/filter.js new file mode 100644 index 000000000..886cd276a --- /dev/null +++ b/lib/register/filter.js @@ -0,0 +1,43 @@ +/* + * Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with + * the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +/** + * Add a custom format to the style dictionary + * @static + * @memberof module:style-dictionary + * @param {Object} filter + * @param {String} filter.name - Name of the filter to be referenced in your config.json + * @param {Function} filter.matcher - Matcher function, return boolean if the property should be included. + * @returns {module:style-dictionary} + * @example + * ```js + * StyleDictionary.registerFilter({ + * name: 'isColor', + * matcher: function(prop) { + * return prop.attributes.category === 'color'; + * } + * }) + * ``` + */ +function registerFilter(options) { + if (typeof options.name !== 'string') + throw new Error('Can\'t register filter; filter.name must be a string'); + if (typeof options.matcher !== 'function') + throw new Error('Can\'t register filter; filter.matcher must be a function'); + + this.filter[options.name] = options.matcher; + + return this; +} + +module.exports = registerFilter; diff --git a/lib/transform/config.js b/lib/transform/config.js index 0a97392de..e55e589cd 100644 --- a/lib/transform/config.js +++ b/lib/transform/config.js @@ -12,14 +12,13 @@ */ var _ = require('lodash'), - chalk = require('chalk'), GroupMessages = require('../utils/groupMessages'); var TEMPLATE_DEPRECATION_WARNINGS = GroupMessages.GROUP.TemplateDeprecationWarnings; /** * Takes a platform config object and returns a new one - * that has transforms, formats, and actions + * that has filters, transforms, formats, and actions * mapped properly. * @private * @param {Object} config @@ -38,8 +37,9 @@ function transformConfig(config, dictionary) { if (to_ret.transforms) { transforms = to_ret.transforms; } else if (to_ret.transformGroup) { - transforms = dictionary.transformGroup[to_ret.transformGroup]; - if (!transforms) { + if (dictionary.transformGroup[to_ret.transformGroup]) { + transforms = dictionary.transformGroup[to_ret.transformGroup]; + } else { throw new Error('transformGroup ' + to_ret.transformGroup + ' doesn\'t exist'); } } @@ -52,25 +52,42 @@ function transformConfig(config, dictionary) { }); to_ret.files = _.map(config.files, function(file) { + const ext = {}; + if (file.filter) { + if(typeof file.filter === 'string') { + if (dictionary.filter[file.filter]) { + ext.filter = dictionary.filter[file.filter]; + } else { + throw new Error('Can\'t find filter: ' + file.filter); + } + } else if (typeof file.filter === 'object') { + ext.filter = _.matches(file.filter); + } else if (typeof file.filter === 'function') { + ext.filter = file.filter; + } else { + throw new Error('Filter format not valid: ' + typeof file.filter); + } + } if (file.template) { if (dictionary.format[file.template]) { GroupMessages.add( TEMPLATE_DEPRECATION_WARNINGS, `${file.destination} (template: ${file.template})` ); - return _.extend({}, file, {format: dictionary.format[file.template]}); + ext.format = dictionary.format[file.template]; } else { throw new Error('Can\'t find template: ' + file.template); } } else if (file.format) { if (dictionary.format[file.format]) { - return _.extend({}, file, {format: dictionary.format[file.format]}); + ext.format = dictionary.format[file.format]; } else { throw new Error('Can\'t find format: ' + file.format); } } else { throw new Error('Please supply a format for file: ' + JSON.stringify(file)); } + return _.extend({}, file, ext); }); to_ret.actions = _.map(config.actions, function(action) { diff --git a/lib/utils/groupMessages.js b/lib/utils/groupMessages.js index bd7a25280..221a89df0 100644 --- a/lib/utils/groupMessages.js +++ b/lib/utils/groupMessages.js @@ -18,6 +18,7 @@ var GroupMessages = { PropertyValueCollisions: 'Property Value Collisions', TemplateDeprecationWarnings: 'Template Deprecation Warnings', RegisterTemplateDeprecationWarnings: 'Register Template Deprecation Warnings', + SassMapFormatDeprecationWarnings: 'Sass Map Format Deprecation Warnings', }, flush: function (messageGroup) {